diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-04 13:35:25 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-04 13:35:25 +0000 |
commit | a8b5f59c0616e8427a52ee1cae15c14caa0e2017 (patch) | |
tree | b689ca1779ca555deedc306a24a6fa01eb46b4ed | |
parent | 3cbc1049e3ef2cbd42577b8bcc7b3ca30749d99c (diff) | |
parent | 2560849bf4526b22306247738d7e409baa23882d (diff) | |
download | avatar-android14-mainline-sdkext-release.tar.gz |
Snap for 11173240 from 2560849bf4526b22306247738d7e409baa23882d to mainline-sdkext-releaseaml_sdk_341510000aml_sdk_341410000android14-mainline-sdkext-release
Change-Id: Ic9090db845376a35b84ee02b5bd21f9f769f62dc
-rw-r--r-- | .github/workflows/avatar.yml | 91 | ||||
-rw-r--r-- | .github/workflows/pypi-publish.yml (renamed from .github/workflows/python-publish.yml) | 2 | ||||
-rw-r--r-- | .github/workflows/python-build.yml | 27 | ||||
-rw-r--r-- | .github/workflows/python-lint-and-format.yml | 34 | ||||
-rw-r--r-- | Android.bp | 6 | ||||
-rw-r--r-- | avatar/__init__.py | 103 | ||||
-rw-r--r-- | avatar/cases/__init__.py (renamed from cases/__init__.py) | 0 | ||||
-rw-r--r-- | avatar/cases/config.yml (renamed from cases/config.yml) | 0 | ||||
-rw-r--r-- | avatar/cases/host_test.py (renamed from cases/host_test.py) | 20 | ||||
-rw-r--r-- | avatar/cases/le_host_test.py (renamed from cases/le_host_test.py) | 15 | ||||
-rw-r--r-- | avatar/cases/le_security_test.py (renamed from cases/le_security_test.py) | 95 | ||||
-rw-r--r-- | avatar/cases/security_test.py (renamed from cases/security_test.py) | 179 | ||||
-rw-r--r-- | avatar/metrics/interceptors.py | 2 | ||||
-rw-r--r-- | avatar/metrics/trace.py | 21 | ||||
-rw-r--r-- | avatar/metrics/trace_pb2.py | 12 | ||||
-rw-r--r-- | avatar/metrics/trace_pb2.pyi | 11 | ||||
-rw-r--r-- | avatar/pandora.py | 67 | ||||
-rw-r--r-- | avatar/pandora_client.py | 8 | ||||
-rw-r--r-- | avatar/pandora_server.py | 6 | ||||
-rw-r--r-- | avatar/runner.py | 140 | ||||
-rw-r--r-- | pyproject.toml | 61 |
21 files changed, 716 insertions, 184 deletions
diff --git a/.github/workflows/avatar.yml b/.github/workflows/avatar.yml new file mode 100644 index 0000000..21945ef --- /dev/null +++ b/.github/workflows/avatar.yml @@ -0,0 +1,91 @@ +name: Avatar + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + name: Build with Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] + steps: + - uses: actions/checkout@v3 + - name: Set Up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install + run: | + pip install --upgrade pip + pip install build + pip install . + - name: Build + run: python -m build + lint: + name: Lint for Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] + steps: + - uses: actions/checkout@v3 + - name: Set Up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install + run: pip install .[dev] + - run: mypy + - run: pyright + format: + name: Check Python formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set Up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - name: Install + run: | + pip install --upgrade pip + pip install .[dev] + - run: black --check avatar/ + - run: isort --check avatar + test: + name: Test Bumble vs Bumble(s) [${{ matrix.shard }}] + runs-on: ubuntu-latest + strategy: + matrix: + shard: [ + 1/24, 2/24, 3/24, 4/24, + 5/24, 6/24, 7/24, 8/24, + 9/24, 10/24, 11/24, 12/24, + 13/24, 14/24, 15/24, 16/24, + 17/24, 18/24, 19/24, 20/24, + 21/24, 22/24, 23/24, 24/24, + ] + steps: + - uses: actions/checkout@v3 + - name: Set Up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - name: Install + run: | + pip install --upgrade pip + pip install rootcanal==1.3.0 + pip install . + - name: Rootcanal + run: nohup python -m rootcanal > rootcanal.log & + - name: Test + run: | + avatar --list | grep -Ev '^=' > test-names.txt + timeout 5m avatar --test-beds bumble.bumbles --tests $(split test-names.txt -n l/${{ matrix.shard }}) + - name: Rootcanal Logs + run: cat rootcanal.log diff --git a/.github/workflows/python-publish.yml b/.github/workflows/pypi-publish.yml index 7d9bc19..36192a9 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -1,4 +1,4 @@ -name: Upload Python Package +name: PyPI Publish on: release: diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml deleted file mode 100644 index 18193b1..0000000 --- a/.github/workflows/python-build.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Python Build - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10", "3.11"] - steps: - - uses: actions/checkout@v3 - - name: Set Up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install build - python -m pip install . - - name: Build - run: python -m build diff --git a/.github/workflows/python-lint-and-format.yml b/.github/workflows/python-lint-and-format.yml deleted file mode 100644 index 8ddff35..0000000 --- a/.github/workflows/python-lint-and-format.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Python Lint & Format - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - lint_and_format: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10", "3.11"] - steps: - - uses: actions/checkout@v3 - - name: Set Up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install .[dev] - - name: Lint with mypy - run: | - mypy - - name: Lint with pyright - run: | - pyright - - name: Check the Format with black and isort - run: | - black --check avatar/ cases/ - isort --check avatar/ cases/ @@ -19,6 +19,7 @@ python_library_host { name: "libavatar", srcs: [ "avatar/*.py", + "avatar/cases/*.py", "avatar/controllers/*.py", "avatar/metrics/*.py", ], @@ -33,8 +34,3 @@ python_library_host { "avatar/py.typed" ] } - -filegroup { - name: "avatar-cases", - srcs: ["cases/*.py"], -} diff --git a/avatar/__init__.py b/avatar/__init__.py index e5a1cf2..abc4653 100644 --- a/avatar/__init__.py +++ b/avatar/__init__.py @@ -17,21 +17,26 @@ Avatar is a scalable multi-platform Bluetooth testing tool capable of running any Bluetooth test cases virtually and physically. """ -__version__ = "0.0.2" +__version__ = "0.0.4" +import argparse import enum import functools import grpc import grpc.aio import importlib import logging +import pathlib from avatar import pandora_server from avatar.aio import asynchronous from avatar.metrics import trace -from avatar.pandora_client import BumblePandoraClient as BumblePandoraDevice, PandoraClient as PandoraDevice +from avatar.pandora_client import BumblePandoraClient as BumblePandoraDevice +from avatar.pandora_client import PandoraClient as PandoraDevice from avatar.pandora_server import PandoraServer +from avatar.runner import SuiteRunner from mobly import base_test +from mobly import signals from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Sized, Tuple, Type, TypeVar # public symbols @@ -105,7 +110,13 @@ class PandoraDevices(Sized, Iterable[PandoraDevice]): # Register the controller and load its Pandora servers. logging.info('Starting %s(s) for %s', server_cls.__name__, controller) - devices: Optional[List[Any]] = test.register_controller(server_cls.MOBLY_CONTROLLER_MODULE) # type: ignore + try: + devices: Optional[List[Any]] = test.register_controller( # type: ignore + server_cls.MOBLY_CONTROLLER_MODULE + ) + except Exception: + logging.exception('abort: failed to register controller') + raise signals.TestAbortAll("") assert devices for device in devices: # type: ignore self._servers.append(server_cls(device)) @@ -215,3 +226,89 @@ def rpc_except( return wrapper return wrap + + +def args_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description='Avatar test runner.') + parser.add_argument( + 'input', + type=str, + nargs='*', + metavar='<PATH>', + help='Lits of folder or test file to run', + default=[], + ) + parser.add_argument('-c', '--config', type=str, metavar='<PATH>', help='Path to the test configuration file.') + parser.add_argument( + '-l', + '--list', + '--list_tests', # For backward compatibility with tradefed `MoblyBinaryHostTest` + action='store_true', + help='Print the names of the tests defined in a script without ' 'executing them.', + ) + parser.add_argument( + '-o', + '--log-path', + '--log_path', # For backward compatibility with tradefed `MoblyBinaryHostTest` + type=str, + metavar='<PATH>', + help='Path to the test configuration file.', + ) + parser.add_argument( + '-t', + '--tests', + nargs='+', + type=str, + metavar='[ClassA[.test_a] ClassB[.test_b] ...]', + help='A list of test classes and optional tests to execute.', + ) + parser.add_argument( + '-b', + '--test-beds', + '--test_bed', # For backward compatibility with tradefed `MoblyBinaryHostTest` + nargs='+', + type=str, + metavar='[<TEST BED NAME1> <TEST BED NAME2> ...]', + help='Specify which test beds to run tests on.', + ) + parser.add_argument('-v', '--verbose', action='store_true', help='Set console logger level to DEBUG') + parser.add_argument('-x', '--no-default-cases', action='store_true', help='Dot no include default test cases') + return parser + + +# Avatar default entry point +def main(args: Optional[argparse.Namespace] = None) -> None: + import sys + + # Create an Avatar suite runner. + runner = SuiteRunner() + + # Parse arguments. + argv = args or args_parser().parse_args() + if argv.input: + for path in argv.input: + runner.add_path(pathlib.Path(path)) + if argv.config: + runner.add_config_file(pathlib.Path(argv.config)) + if argv.log_path: + runner.set_logs_dir(pathlib.Path(argv.log_path)) + if argv.tests: + runner.add_test_filters(argv.tests) + if argv.test_beds: + runner.add_test_beds(argv.test_beds) + if argv.verbose: + runner.set_logs_verbose() + if not argv.no_default_cases: + runner.add_path(pathlib.Path(__file__).resolve().parent / 'cases') + + # List tests to standard output. + if argv.list: + for _, (tag, test_names) in runner.included_tests.items(): + for name in test_names: + print(f"{tag}.{name}") + sys.exit(0) + + # Run the test suite. + logging.basicConfig(level=logging.INFO) + if not runner.run(): + sys.exit(1) diff --git a/cases/__init__.py b/avatar/cases/__init__.py index bb545f4..bb545f4 100644 --- a/cases/__init__.py +++ b/avatar/cases/__init__.py diff --git a/cases/config.yml b/avatar/cases/config.yml index a5451cd..a5451cd 100644 --- a/cases/config.yml +++ b/avatar/cases/config.yml diff --git a/cases/host_test.py b/avatar/cases/host_test.py index 59e5100..feaeaa5 100644 --- a/cases/host_test.py +++ b/avatar/cases/host_test.py @@ -17,21 +17,23 @@ import avatar import grpc import logging -from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices -from mobly import base_test, signals, test_runner +from avatar import BumblePandoraDevice +from avatar import PandoraDevice +from avatar import PandoraDevices +from mobly import base_test +from mobly import signals +from mobly import test_runner from mobly.asserts import assert_equal # type: ignore from mobly.asserts import assert_false # type: ignore from mobly.asserts import assert_is_none # type: ignore from mobly.asserts import assert_is_not_none # type: ignore from mobly.asserts import assert_true # type: ignore from mobly.asserts import explicit_pass # type: ignore -from pandora.host_pb2 import ( - DISCOVERABLE_GENERAL, - DISCOVERABLE_LIMITED, - NOT_DISCOVERABLE, - Connection, - DiscoverabilityMode, -) +from pandora.host_pb2 import DISCOVERABLE_GENERAL +from pandora.host_pb2 import DISCOVERABLE_LIMITED +from pandora.host_pb2 import NOT_DISCOVERABLE +from pandora.host_pb2 import Connection +from pandora.host_pb2 import DiscoverabilityMode from typing import Optional diff --git a/cases/le_host_test.py b/avatar/cases/le_host_test.py index eb2f017..b6ab4aa 100644 --- a/cases/le_host_test.py +++ b/avatar/cases/le_host_test.py @@ -20,14 +20,21 @@ import itertools import logging import random -from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices -from mobly import base_test, test_runner +from avatar import BumblePandoraDevice +from avatar import PandoraDevice +from avatar import PandoraDevices +from mobly import base_test +from mobly import test_runner from mobly.asserts import assert_equal # type: ignore from mobly.asserts import assert_false # type: ignore from mobly.asserts import assert_is_not_none # type: ignore from mobly.asserts import assert_true # type: ignore from mobly.asserts import explicit_pass # type: ignore -from pandora.host_pb2 import PUBLIC, RANDOM, Connection, DataTypes, OwnAddressType +from pandora.host_pb2 import PUBLIC +from pandora.host_pb2 import RANDOM +from pandora.host_pb2 import Connection +from pandora.host_pb2 import DataTypes +from pandora.host_pb2 import OwnAddressType from typing import Any, Dict, Literal, Optional, Union @@ -174,7 +181,7 @@ class LeHostTest(base_test.BaseTestClass): # type: ignore[misc] assert_true(report.legacy, msg='expected legacy advertising report') assert_equal(report.connectable, True) - for (key, value) in data.items(): + for key, value in data.items(): assert_equal(getattr(report.data, key), value) # type: ignore[misc] assert_false(report.truncated, msg='expected non-truncated advertising report') diff --git a/cases/le_security_test.py b/avatar/cases/le_security_test.py index 9c836b2..b91d8c7 100644 --- a/cases/le_security_test.py +++ b/avatar/cases/le_security_test.py @@ -17,15 +17,29 @@ import avatar import itertools import logging -from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices +from avatar import BumblePandoraDevice +from avatar import PandoraDevice +from avatar import PandoraDevices +from avatar import pandora +from bumble.pairing import PairingConfig from bumble.pairing import PairingDelegate -from mobly import base_test, signals, test_runner +from mobly import base_test +from mobly import signals +from mobly import test_runner from mobly.asserts import assert_equal # type: ignore from mobly.asserts import assert_in # type: ignore from mobly.asserts import assert_is_not_none # type: ignore from mobly.asserts import fail # type: ignore -from pandora.host_pb2 import PUBLIC, RANDOM, Connection, DataTypes, OwnAddressType -from pandora.security_pb2 import LE_LEVEL3, PairingEventAnswer, SecureResponse, WaitSecurityResponse +from pandora.host_pb2 import PUBLIC +from pandora.host_pb2 import RANDOM +from pandora.host_pb2 import Connection +from pandora.host_pb2 import DataTypes +from pandora.host_pb2 import OwnAddressType +from pandora.security_pb2 import LE_LEVEL3 +from pandora.security_pb2 import LEVEL2 +from pandora.security_pb2 import PairingEventAnswer +from pandora.security_pb2 import SecureResponse +from pandora.security_pb2 import WaitSecurityResponse from typing import Any, Literal, Optional, Tuple, Union @@ -74,6 +88,10 @@ class LeSecurityTest(base_test.BaseTestClass): # type: ignore[misc] 'against_display_yes_no', 'against_both_display_and_keyboard', ), + ( + 'ltk_irk_csrk', + 'ltk_irk_csrk_lk', + ), ) ) # type: ignore[misc] @avatar.asynchronous @@ -84,6 +102,7 @@ class LeSecurityTest(base_test.BaseTestClass): # type: ignore[misc] ref_address_type_name: Union[Literal['against_random'], Literal['against_public']], variant: Union[ Literal['accept'], + Literal['accept_ctkd'], Literal['reject'], Literal['rejected'], Literal['disconnect'], @@ -97,8 +116,11 @@ class LeSecurityTest(base_test.BaseTestClass): # type: ignore[misc] Literal['against_display_yes_no'], Literal['against_both_display_and_keyboard'], ], + key_distribution: Union[ + Literal['ltk_irk_csrk'], + Literal['ltk_irk_csrk_lk'], + ], ) -> None: - if self.dut.name == 'android' and connect == 'outgoing_connection' and pair == 'incoming_pairing': # TODO: do not skip when doing physical tests. raise signals.TestSkip('TODO: Yet to implement the test cases:\n') @@ -114,18 +136,21 @@ class LeSecurityTest(base_test.BaseTestClass): # type: ignore[misc] + '- When disconnected the `Secure/WaitSecurity` never returns.' ) + if self.dut.name == 'android' and 'reject' in variant: + raise signals.TestSkip('TODO: Currently these scnearios are not working. Working on them.') + if self.ref.name == 'android' and ref_address_type_name == 'against_public': raise signals.TestSkip('Android does not support PUBLIC address type.') - if 'reject' in variant or 'rejected' in variant: - raise signals.TestSkip('TODO: Currently these scnearios are not working. Working on them.') - if isinstance(self.ref, BumblePandoraDevice) and ref_io_capability == 'against_default_io_cap': raise signals.TestSkip('Skip default IO cap for Bumble REF.') if not isinstance(self.ref, BumblePandoraDevice) and ref_io_capability != 'against_default_io_cap': raise signals.TestSkip('Unable to override IO capability on non Bumble device.') + if 'lk' in key_distribution and ref_io_capability == 'against_no_output_no_input': + raise signals.TestSkip('CTKD requires Security Level 4') + # Factory reset both DUT and REF devices. await asyncio.gather(self.dut.reset(), self.ref.reset()) @@ -139,6 +164,30 @@ class LeSecurityTest(base_test.BaseTestClass): # type: ignore[misc] 'against_both_display_and_keyboard': PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT, }[ref_io_capability] self.ref.server_config.io_capability = io_capability + bumble_key_distribution = sum( + { + 'ltk': PairingDelegate.KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY, + 'irk': PairingDelegate.KeyDistribution.DISTRIBUTE_IDENTITY_KEY, + 'csrk': PairingDelegate.KeyDistribution.DISTRIBUTE_SIGNING_KEY, + 'lk': PairingDelegate.KeyDistribution.DISTRIBUTE_LINK_KEY, + }[x] + for x in key_distribution.split('_') + ) + assert bumble_key_distribution + self.ref.server_config.smp_local_initiator_key_distribution = bumble_key_distribution + self.ref.server_config.smp_local_responder_key_distribution = bumble_key_distribution + self.ref.server_config.identity_address_type = PairingConfig.AddressType.PUBLIC + + if isinstance(self.dut, BumblePandoraDevice): + ALL_KEYS = ( + PairingDelegate.KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY + | PairingDelegate.KeyDistribution.DISTRIBUTE_IDENTITY_KEY + | PairingDelegate.KeyDistribution.DISTRIBUTE_SIGNING_KEY + | PairingDelegate.KeyDistribution.DISTRIBUTE_LINK_KEY + ) + self.dut.server_config.smp_local_initiator_key_distribution = ALL_KEYS + self.dut.server_config.smp_local_responder_key_distribution = ALL_KEYS + self.dut.server_config.identity_address_type = PairingConfig.AddressType.PUBLIC dut_address_type = RANDOM ref_address_type = { @@ -161,7 +210,6 @@ class LeSecurityTest(base_test.BaseTestClass): # type: ignore[misc] initiator_addr_type: OwnAddressType, acceptor_addr_type: OwnAddressType, ) -> Tuple[Connection, Connection]: - # Acceptor - Advertise advertisement = acceptor.aio.host.Advertise( legacy=True, @@ -172,24 +220,13 @@ class LeSecurityTest(base_test.BaseTestClass): # type: ignore[misc] # Initiator - Scan and fetch the address scan = initiator.aio.host.Scan(own_address_type=initiator_addr_type) - acceptor_addr = await anext( + acceptor_scan = await anext( (x async for x in scan if b'pause cafe' in x.data.manufacturer_specific_data) ) # pytype: disable=name-error scan.cancel() # Initiator - LE connect - init_res, wait_res = await asyncio.gather( - initiator.aio.host.ConnectLE( - own_address_type=initiator_addr_type, **acceptor_addr.address_asdict() - ), - anext(aiter(advertisement)), # pytype: disable=name-error - ) - - advertisement.cancel() - assert_equal(init_res.result_variant(), 'connection') - - assert init_res.connection is not None and wait_res.connection is not None - return init_res.connection, wait_res.connection + return await pandora.connect_le(initiator, advertisement, acceptor_scan, initiator_addr_type) # Make LE connection. if connect == 'incoming_connection': @@ -329,6 +366,20 @@ class LeSecurityTest(base_test.BaseTestClass): # type: ignore[misc] if shall_pass: assert_equal(secure.result_variant(), 'success') assert_equal(wait_security.result_variant(), 'success') + if 'lk' in key_distribution: + # Make a Classic connection + if self.dut.name == 'android': + # Android IOP: Android automatically trigger a BR/EDR connection request + # in this case. + ref_dut_classic_res = await self.ref.aio.host.WaitConnection(self.dut.address) + assert_is_not_none(ref_dut_classic_res.connection) + assert ref_dut_classic_res.connection + ref_dut_classic = ref_dut_classic_res.connection + else: + ref_dut_classic, _ = await pandora.connect(self.ref, self.dut) + # Try to encrypt Classic connection + ref_dut_secure = await self.ref.aio.security.Secure(ref_dut_classic, classic=LEVEL2) + assert_equal(ref_dut_secure.result_variant(), 'success') else: assert_in( secure.result_variant(), diff --git a/cases/security_test.py b/avatar/cases/security_test.py index 4d23b0c..5213119 100644 --- a/cases/security_test.py +++ b/avatar/cases/security_test.py @@ -17,17 +17,67 @@ import avatar import itertools import logging -from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices -from bumble.hci import HCI_CENTRAL_ROLE, HCI_PERIPHERAL_ROLE +from avatar import BumblePandoraDevice +from avatar import PandoraDevice +from avatar import PandoraDevices +from avatar import pandora +from bumble.hci import HCI_CENTRAL_ROLE +from bumble.hci import HCI_PERIPHERAL_ROLE +from bumble.hci import HCI_Write_Default_Link_Policy_Settings_Command +from bumble.keys import PairingKeys +from bumble.pairing import PairingConfig from bumble.pairing import PairingDelegate -from mobly import base_test, signals, test_runner +from mobly import base_test +from mobly import signals +from mobly import test_runner from mobly.asserts import assert_equal # type: ignore from mobly.asserts import assert_in # type: ignore from mobly.asserts import assert_is_not_none # type: ignore from mobly.asserts import fail # type: ignore -from pandora.host_pb2 import Connection -from pandora.security_pb2 import LEVEL2, PairingEventAnswer, SecureResponse, WaitSecurityResponse -from typing import Any, Literal, Optional, Tuple, Union +from pandora.host_pb2 import RANDOM +from pandora.host_pb2 import RESOLVABLE_OR_PUBLIC +from pandora.host_pb2 import Connection as PandoraConnection +from pandora.host_pb2 import DataTypes +from pandora.security_pb2 import LE_LEVEL2 +from pandora.security_pb2 import LEVEL2 +from pandora.security_pb2 import PairingEventAnswer +from pandora.security_pb2 import SecureResponse +from pandora.security_pb2 import WaitSecurityResponse +from typing import Any, List, Literal, Optional, Tuple, Union + +DEFAULT_SMP_KEY_DISTRIBUTION = ( + PairingDelegate.KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY + | PairingDelegate.KeyDistribution.DISTRIBUTE_IDENTITY_KEY + | PairingDelegate.KeyDistribution.DISTRIBUTE_SIGNING_KEY +) + + +async def le_connect_with_rpa_and_encrypt(central: PandoraDevice, peripheral: PandoraDevice) -> None: + # Note: Android doesn't support own_address_type=RESOLVABLE_OR_PUBLIC(offloaded resolution) + # But own_address_type=RANDOM still set a public RPA generated in host + advertisement = peripheral.aio.host.Advertise( + legacy=True, + connectable=True, + own_address_type=RANDOM if peripheral.name == 'android' else RESOLVABLE_OR_PUBLIC, + data=DataTypes(manufacturer_specific_data=b'pause cafe'), + ) + + (cen_res, per_res) = await asyncio.gather( + central.aio.host.ConnectLE( + own_address_type=RANDOM if central.name == 'android' else RESOLVABLE_OR_PUBLIC, + public=peripheral.address, + ), + anext(aiter(advertisement)), # pytype: disable=name-error + ) + + advertisement.cancel() + assert_equal(cen_res.result_variant(), 'connection') + cen_per = cen_res.connection + per_cen = per_res.connection + assert cen_per is not None and per_cen is not None + + encryption = await peripheral.aio.security.Secure(connection=per_cen, le=LE_LEVEL2) + assert_equal(encryption.result_variant(), 'success') class SecurityTest(base_test.BaseTestClass): # type: ignore[misc] @@ -50,6 +100,7 @@ class SecurityTest(base_test.BaseTestClass): # type: ignore[misc] # Enable BR/EDR mode and SSP for Bumble devices. for device in self.devices: if isinstance(device, BumblePandoraDevice): + device.config.setdefault('address_resolution_offload', True) device.config.setdefault('classic_enabled', True) device.config.setdefault('classic_ssp_enabled', True) device.config.setdefault( @@ -75,6 +126,7 @@ class SecurityTest(base_test.BaseTestClass): # type: ignore[misc] 'rejected', 'disconnect', 'disconnected', + 'accept_ctkd', ), ( 'against_default_io_cap', @@ -97,6 +149,7 @@ class SecurityTest(base_test.BaseTestClass): # type: ignore[misc] Literal['rejected'], Literal['disconnect'], Literal['disconnected'], + Literal['accept_ctkd'], ], ref_io_capability: Union[ Literal['against_default_io_cap'], @@ -142,6 +195,10 @@ class SecurityTest(base_test.BaseTestClass): # type: ignore[misc] if not isinstance(self.ref, BumblePandoraDevice) and ref_io_capability != 'against_default_io_cap': raise signals.TestSkip('Unable to override IO capability on non Bumble device.') + # CTKD + if 'ctkd' in variant and ref_io_capability not in ('against_display_yes_no'): + raise signals.TestSkip('CTKD cases must be conducted under Security Level 4') + # Factory reset both DUT and REF devices. await asyncio.gather(self.dut.reset(), self.ref.reset()) @@ -154,48 +211,91 @@ class SecurityTest(base_test.BaseTestClass): # type: ignore[misc] 'against_display_yes_no': PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT, }[ref_io_capability] self.ref.server_config.io_capability = io_capability + self.ref.server_config.smp_local_initiator_key_distribution = DEFAULT_SMP_KEY_DISTRIBUTION + self.ref.server_config.smp_local_responder_key_distribution = DEFAULT_SMP_KEY_DISTRIBUTION + # Distribute Public identity address + self.ref.server_config.identity_address_type = PairingConfig.AddressType.PUBLIC + # Allow role switch + # TODO: Remove direct Bumble usage + await self.ref.device.send_command(HCI_Write_Default_Link_Policy_Settings_Command(default_link_policy_settings=0x01), check_result=True) # type: ignore + + # Override DUT Bumble device capabilities. + if isinstance(self.dut, BumblePandoraDevice): + self.dut.server_config.smp_local_initiator_key_distribution = DEFAULT_SMP_KEY_DISTRIBUTION + self.dut.server_config.smp_local_responder_key_distribution = DEFAULT_SMP_KEY_DISTRIBUTION + # Distribute Public identity address + self.dut.server_config.identity_address_type = PairingConfig.AddressType.PUBLIC + # Allow role switch + # TODO: Remove direct Bumble usage + await self.dut.device.send_command(HCI_Write_Default_Link_Policy_Settings_Command(default_link_policy_settings=0x01), check_result=True) # type: ignore # Pandora connection tokens - ref_dut, dut_ref = None, None + ref_dut: Optional[PandoraConnection] = None + dut_ref: Optional[PandoraConnection] = None + # Bumble connection + ref_dut_bumble = None + dut_ref_bumble = None + # CTKD async task + ctkd_task = None + need_ctkd = 'ctkd' in variant # Connection/pairing task. async def connect_and_pair() -> Tuple[SecureResponse, WaitSecurityResponse]: nonlocal ref_dut nonlocal dut_ref - - # Make classic connection task. - async def bredr_connect( - initiator: PandoraDevice, acceptor: PandoraDevice - ) -> Tuple[Connection, Connection]: - init_res, wait_res = await asyncio.gather( - initiator.aio.host.Connect(address=acceptor.address), - acceptor.aio.host.WaitConnection(address=initiator.address), - ) - assert_equal(init_res.result_variant(), 'connection') - assert_equal(wait_res.result_variant(), 'connection') - assert init_res.connection is not None and wait_res.connection is not None - return init_res.connection, wait_res.connection + nonlocal ref_dut_bumble + nonlocal dut_ref_bumble + nonlocal ctkd_task # Make classic connection. if connect == 'incoming_connection': - ref_dut, dut_ref = await bredr_connect(self.ref, self.dut) + ref_dut, dut_ref = await pandora.connect(initiator=self.ref, acceptor=self.dut) else: - dut_ref, ref_dut = await bredr_connect(self.dut, self.ref) + dut_ref, ref_dut = await pandora.connect(initiator=self.dut, acceptor=self.ref) + # Retrieve Bumble connection + if isinstance(self.dut, BumblePandoraDevice): + dut_ref_bumble = pandora.get_raw_connection(self.dut, dut_ref) # Role switch. if isinstance(self.ref, BumblePandoraDevice): - ref_dut_raw = self.ref.device.lookup_connection(int.from_bytes(ref_dut.cookie.value, 'big')) - if ref_dut_raw is not None: + ref_dut_bumble = pandora.get_raw_connection(self.ref, ref_dut) + if ref_dut_bumble is not None: role = { 'against_central': HCI_CENTRAL_ROLE, 'against_peripheral': HCI_PERIPHERAL_ROLE, }[ref_role] - if ref_dut_raw.role != role: + if ref_dut_bumble.role != role: self.ref.log.info( f"Role switch to: {'`CENTRAL`' if role == HCI_CENTRAL_ROLE else '`PERIPHERAL`'}" ) - await ref_dut_raw.switch_role(role) + await ref_dut_bumble.switch_role(role) + + # TODO: Remove direct Bumble usage + async def wait_ctkd_keys() -> List[PairingKeys]: + futures: List[asyncio.Future[PairingKeys]] = [] + if ref_dut_bumble is not None: + ref_dut_fut = asyncio.get_event_loop().create_future() + futures.append(ref_dut_fut) + + def on_pairing(keys: PairingKeys) -> None: + ref_dut_fut.set_result(keys) + + ref_dut_bumble.on('pairing', on_pairing) + if dut_ref_bumble is not None: + dut_ref_fut = asyncio.get_event_loop().create_future() + futures.append(dut_ref_fut) + + def on_pairing(keys: PairingKeys) -> None: + dut_ref_fut.set_result(keys) + + dut_ref_bumble.on('pairing', on_pairing) + + return await asyncio.gather(*futures) + + if need_ctkd: + # CTKD might be triggered by devices automatically, so CTKD listener must be started here + ctkd_task = asyncio.create_task(wait_ctkd_keys()) # Pairing. if pair == 'incoming_pairing': @@ -215,7 +315,7 @@ class SecurityTest(base_test.BaseTestClass): # type: ignore[misc] # Start connection/pairing. connect_and_pair_task = asyncio.create_task(connect_and_pair()) - shall_pass = variant == 'accept' + shall_pass = variant == 'accept' or 'ctkd' in variant try: dut_pairing_fut = asyncio.create_task(anext(dut_pairing)) ref_pairing_fut = asyncio.create_task(anext(ref_pairing)) @@ -339,6 +439,31 @@ class SecurityTest(base_test.BaseTestClass): # type: ignore[misc] dut_pairing.cancel() ref_pairing.cancel() + if not need_ctkd: + return + + ctkd_shall_pass = variant == 'accept_ctkd' + + if variant == 'accept_ctkd': + # TODO: Remove direct Bumble usage + async def ctkd_over_bredr() -> None: + if ref_role == 'against_central': + if ref_dut_bumble is not None: + await ref_dut_bumble.pair() + else: + if dut_ref_bumble is not None: + await dut_ref_bumble.pair() + assert ctkd_task is not None + await ctkd_task + + await ctkd_over_bredr() + else: + fail("Unsupported variant " + variant) + + if ctkd_shall_pass: + # Try to connect with RPA(to verify IRK), and encrypt(to verify LTK) + await le_connect_with_rpa_and_encrypt(self.dut, self.ref) + if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) diff --git a/avatar/metrics/interceptors.py b/avatar/metrics/interceptors.py index e853395..3ac7da1 100644 --- a/avatar/metrics/interceptors.py +++ b/avatar/metrics/interceptors.py @@ -193,7 +193,6 @@ class AioUnaryStreamInterceptor(grpc.aio.UnaryStreamClientInterceptor): # type: client_call_details: ClientCallDetails, request: _T, ) -> utils.AioStream[_U]: - # TODO: this is a workaround for https://github.com/grpc/grpc/pull/33951 # need to be deleted as soon as `grpcio` contains the fix. now = time.time() @@ -241,7 +240,6 @@ class AioStreamStreamInterceptor(grpc.aio.StreamStreamClientInterceptor): # typ client_call_details: ClientCallDetails, request: utils.AioSender[_T], ) -> utils.AioStreamStream[_T, _U]: - # TODO: this is a workaround for https://github.com/grpc/grpc/pull/33951 # need to be deleted as soon as `grpcio` contains the fix. now = time.time() diff --git a/avatar/metrics/trace.py b/avatar/metrics/trace.py index 52f2bc6..86bc21a 100644 --- a/avatar/metrics/trace.py +++ b/avatar/metrics/trace.py @@ -18,16 +18,15 @@ import atexit import time import types -from avatar.metrics.trace_pb2 import ( - DebugAnnotation, - ProcessDescriptor, - ThreadDescriptor, - Trace, - TracePacket, - TrackDescriptor, - TrackEvent, -) -from google.protobuf import any_pb2, message +from avatar.metrics.trace_pb2 import DebugAnnotation +from avatar.metrics.trace_pb2 import ProcessDescriptor +from avatar.metrics.trace_pb2 import ThreadDescriptor +from avatar.metrics.trace_pb2 import Trace +from avatar.metrics.trace_pb2 import TracePacket +from avatar.metrics.trace_pb2 import TrackDescriptor +from avatar.metrics.trace_pb2 import TrackEvent +from google.protobuf import any_pb2 +from google.protobuf import message from mobly.base_test import BaseTestClass from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Tuple, Union @@ -264,7 +263,7 @@ def debug_value(v: Any) -> Tuple[Any, Dict[str, Any]]: def debug_message(msg: message.Message) -> Tuple[Dict[str, Any], List[DebugAnnotation]]: json: Dict[str, Any] = {} dbga: List[DebugAnnotation] = [] - for (f, v) in msg.ListFields(): + for f, v in msg.ListFields(): if ( isinstance(v, bytes) and len(v) == 6 diff --git a/avatar/metrics/trace_pb2.py b/avatar/metrics/trace_pb2.py index 095c6e4..7743384 100644 --- a/avatar/metrics/trace_pb2.py +++ b/avatar/metrics/trace_pb2.py @@ -1,12 +1,13 @@ +# pyright: reportGeneralTypeIssues=false +# pyright: reportUnknownVariableType=false +# pyright: reportUnknownMemberType=false # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: trace.proto """Generated protocol buffer code.""" -from google.protobuf import ( - descriptor as _descriptor, - descriptor_pool as _descriptor_pool, - symbol_database as _symbol_database, -) +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) @@ -22,7 +23,6 @@ _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'trace_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None _globals['_TRACE']._serialized_start = 32 _globals['_TRACE']._serialized_end = 85 diff --git a/avatar/metrics/trace_pb2.pyi b/avatar/metrics/trace_pb2.pyi index e8aff00..fcfac67 100644 --- a/avatar/metrics/trace_pb2.pyi +++ b/avatar/metrics/trace_pb2.pyi @@ -1,6 +1,11 @@ -from google.protobuf import descriptor as _descriptor, message as _message -from google.protobuf.internal import containers as _containers, enum_type_wrapper as _enum_type_wrapper -from typing import ClassVar as _ClassVar, Iterable as _Iterable, Optional as _Optional, Union as _Union +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from typing import ClassVar as _ClassVar +from typing import Iterable as _Iterable +from typing import Optional as _Optional +from typing import Union as _Union DESCRIPTOR: _descriptor.FileDescriptor diff --git a/avatar/pandora.py b/avatar/pandora.py new file mode 100644 index 0000000..31695ee --- /dev/null +++ b/avatar/pandora.py @@ -0,0 +1,67 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio + +from avatar import BumblePandoraDevice +from avatar import PandoraDevice +from bumble.device import Connection as BumbleConnection +from mobly.asserts import assert_equal # type: ignore +from mobly.asserts import assert_is_not_none # type: ignore +from pandora._utils import AioStream +from pandora.host_pb2 import AdvertiseResponse +from pandora.host_pb2 import Connection +from pandora.host_pb2 import OwnAddressType +from pandora.host_pb2 import ScanningResponse +from typing import Optional, Tuple + + +def get_raw_connection_handle(device: PandoraDevice, connection: Connection) -> int: + assert isinstance(device, BumblePandoraDevice) + return int.from_bytes(connection.cookie.value, 'big') + + +def get_raw_connection(device: PandoraDevice, connection: Connection) -> Optional[BumbleConnection]: + assert isinstance(device, BumblePandoraDevice) + return device.device.lookup_connection(get_raw_connection_handle(device, connection)) + + +async def connect(initiator: PandoraDevice, acceptor: PandoraDevice) -> Tuple[Connection, Connection]: + init_res, wait_res = await asyncio.gather( + initiator.aio.host.Connect(address=acceptor.address), + acceptor.aio.host.WaitConnection(address=initiator.address), + ) + assert_equal(init_res.result_variant(), 'connection') + assert_equal(wait_res.result_variant(), 'connection') + assert init_res.connection is not None and wait_res.connection is not None + return init_res.connection, wait_res.connection + + +async def connect_le( + initiator: PandoraDevice, + acceptor: AioStream[AdvertiseResponse], + scan: ScanningResponse, + own_address_type: OwnAddressType, + cancel_advertisement: bool = True, +) -> Tuple[Connection, Connection]: + (init_res, wait_res) = await asyncio.gather( + initiator.aio.host.ConnectLE(own_address_type=own_address_type, **scan.address_asdict()), + anext(aiter(acceptor)), # pytype: disable=name-error + ) + if cancel_advertisement: + acceptor.cancel() + assert_equal(init_res.result_variant(), 'connection') + assert_is_not_none(init_res.connection) + assert init_res.connection + return init_res.connection, wait_res.connection diff --git a/avatar/pandora_client.py b/avatar/pandora_client.py index 60689aa..98211c6 100644 --- a/avatar/pandora_client.py +++ b/avatar/pandora_client.py @@ -23,12 +23,16 @@ import grpc import grpc.aio import logging -from avatar.metrics.interceptors import aio_interceptors, interceptors +from avatar.metrics.interceptors import aio_interceptors +from avatar.metrics.interceptors import interceptors from bumble import pandora as bumble_server from bumble.hci import Address as BumbleAddress from bumble.pandora.device import PandoraDevice as BumblePandoraDevice from dataclasses import dataclass -from pandora import host_grpc, host_grpc_aio, security_grpc, security_grpc_aio +from pandora import host_grpc +from pandora import host_grpc_aio +from pandora import security_grpc +from pandora import security_grpc_aio from typing import Any, Dict, MutableMapping, Optional, Tuple, Union diff --git a/avatar/pandora_server.py b/avatar/pandora_server.py index 4fd56fb..aafc3fc 100644 --- a/avatar/pandora_server.py +++ b/avatar/pandora_server.py @@ -23,8 +23,10 @@ import portpicker import threading import types -from avatar.controllers import bumble_device, pandora_device -from avatar.pandora_client import BumblePandoraClient, PandoraClient +from avatar.controllers import bumble_device +from avatar.controllers import pandora_device +from avatar.pandora_client import BumblePandoraClient +from avatar.pandora_client import PandoraClient from bumble import pandora as bumble_server from bumble.pandora.device import PandoraDevice as BumblePandoraDevice from contextlib import suppress diff --git a/avatar/runner.py b/avatar/runner.py new file mode 100644 index 0000000..8cc4a53 --- /dev/null +++ b/avatar/runner.py @@ -0,0 +1,140 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Avatar runner.""" + +import inspect +import logging +import os +import pathlib + +from importlib.machinery import SourceFileLoader +from mobly import base_test +from mobly import config_parser +from mobly import signals +from mobly import test_runner +from typing import Dict, List, Tuple, Type + +_BUMBLE_BTSNOOP_FMT = 'bumble_btsnoop_{pid}_{instance}.log' + + +class SuiteRunner: + test_beds: List[str] = [] + test_run_configs: List[config_parser.TestRunConfig] = [] + test_classes: List[Type[base_test.BaseTestClass]] = [] + test_filters: List[str] = [] + logs_dir: pathlib.Path = pathlib.Path('out') + logs_verbose: bool = False + + def set_logs_dir(self, path: pathlib.Path) -> None: + self.logs_dir = path + + def set_logs_verbose(self, verbose: bool = True) -> None: + self.logs_verbose = verbose + + def add_test_beds(self, test_beds: List[str]) -> None: + self.test_beds += test_beds + + def add_test_filters(self, test_filters: List[str]) -> None: + self.test_filters += test_filters + + def add_config_file(self, path: pathlib.Path) -> None: + self.test_run_configs += config_parser.load_test_config_file(str(path)) # type: ignore + + def add_test_class(self, cls: Type[base_test.BaseTestClass]) -> None: + self.test_classes.append(cls) + + def add_test_module(self, path: pathlib.Path) -> None: + try: + module = SourceFileLoader(path.stem, str(path)).load_module() + classes = inspect.getmembers(module, inspect.isclass) + for _, cls in classes: + if issubclass(cls, base_test.BaseTestClass): + self.test_classes.append(cls) + except ImportError: + pass + + def add_path(self, path: pathlib.Path, root: bool = True) -> None: + if path.is_file(): + if path.name.endswith('_test.py'): + self.add_test_module(path) + elif not self.test_run_configs and not root and path.name in ('config.yml', 'config.yaml'): + self.add_config_file(path) + elif root: + raise ValueError(f'{path} is not a test file') + else: + for child in path.iterdir(): + self.add_path(child, root=False) + + def is_included(self, cls: base_test.BaseTestClass, test: str) -> bool: + return not self.test_filters or any(filter_match(cls, test, filter) for filter in self.test_filters) + + @property + def included_tests(self) -> Dict[Type[base_test.BaseTestClass], Tuple[str, List[str]]]: + result: Dict[Type[base_test.BaseTestClass], Tuple[str, List[str]]] = {} + for test_class in self.test_classes: + cls = test_class(config_parser.TestRunConfig()) + test_names: List[str] = [] + try: + # Executes pre-setup procedures, this is required since it might + # generate test methods that we want to return as well. + cls._pre_run() + test_names = cls.tests or cls.get_existing_test_names() # type: ignore + test_names = list(test for test in test_names if self.is_included(cls, test)) + if test_names: + assert cls.TAG + result[test_class] = (cls.TAG, test_names) + except Exception: + logging.exception('Failed to retrieve generated tests.') + finally: + cls._clean_up() + return result + + def run(self) -> bool: + # Create logs directory. + if not self.logs_dir.exists(): + self.logs_dir.mkdir() + + # Enable Bumble snoop logs. + os.environ.setdefault('BUMBLE_SNOOPER', f'btsnoop:file:{self.logs_dir}/{_BUMBLE_BTSNOOP_FMT}') + + # Execute the suite + ok = True + for config in self.test_run_configs: + test_bed: str = config.test_bed_name # type: ignore + if self.test_beds and test_bed not in self.test_beds: + continue + runner = test_runner.TestRunner(config.log_path, config.testbed_name) + with runner.mobly_logger(console_level=logging.DEBUG if self.logs_verbose else logging.INFO): + for test_class, (_, tests) in self.included_tests.items(): + runner.add_test_class(config, test_class, tests) # type: ignore + try: + runner.run() + ok = ok and runner.results.is_all_pass + except signals.TestAbortAll: + ok = ok and not self.test_beds + except Exception: + logging.exception('Exception when executing %s.', config.testbed_name) + ok = False + return ok + + +def filter_match(cls: base_test.BaseTestClass, test: str, filter: str) -> bool: + tag: str = cls.TAG # type: ignore + if '.test_' in filter: + return f"{tag}.{test}".startswith(filter) + if filter.startswith('test_'): + return test.startswith(filter) + return tag.startswith(filter) diff --git a/pyproject.toml b/pyproject.toml index cba7d27..496da6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,47 +10,66 @@ classifiers = [ ] dependencies = [ "bt-test-interfaces", - "bumble==0.0.154", - "protobuf>=4.22.0", - "grpcio>=1.51.1", - "mobly>=1.12", + "bumble==0.0.170", + "protobuf==4.24.2", + "grpcio==1.57", + "mobly==1.12.2", "portpicker>=1.5.2", ] [project.urls] Source = "https://github.com/google/avatar" +[project.scripts] +avatar = "avatar:main" + [project.optional-dependencies] dev = [ - "grpcio-tools>=1.51.1", - "black==22.10.0", + "rootcanal==1.3.0", + "grpcio-tools>=1.57", "pyright==1.1.298", - "mypy==1.0", + "mypy==1.5.1", + "black==23.7.0", "isort==5.12.0", - "types-psutil>=5.9.5.6", - "types-setuptools>=65.7.0.3", - "types-protobuf>=4.21.0.3" + "types-psutil==5.9.5.16", + "types-setuptools==68.1.0.1", + "types-protobuf==4.24.0.1" ] +[tool.flit.module] +name = "avatar" + +[tool.flit.sdist] +include = ["doc/"] + [tool.black] line-length = 119 target-version = ["py38", "py39", "py310", "py311"] skip-string-normalization = true -[tool.flit.module] -name = "avatar" - [tool.isort] profile = "black" line_length = 119 no_sections = true lines_between_types = 1 -combine_as_imports = true +force_single_line = true +single_line_exclusions = ["typing", "typing_extensions", "collections.abc"] + +[tool.pyright] +include = ["avatar"] +exclude = ["**/__pycache__", "**/*_pb2.py"] +typeCheckingMode = "strict" +useLibraryCodeForTypes = true +verboseOutput = false +reportMissingTypeStubs = false +reportUnknownLambdaType = false +reportImportCycles = false +reportPrivateUsage = false [tool.mypy] strict = true warn_unused_ignores = false -files = ["avatar", "cases"] +files = ["avatar"] [[tool.mypy.overrides]] module = "grpc.*" @@ -68,18 +87,8 @@ ignore_missing_imports = true module = "portpicker.*" ignore_missing_imports = true -[tool.pyright] -include = ["avatar", "cases"] -exclude = ["**/__pycache__", "**/*_pb2.py"] -typeCheckingMode = "strict" -useLibraryCodeForTypes = true -verboseOutput = false -reportMissingTypeStubs = false -reportUnknownLambdaType = false -reportImportCycles = false - [tool.pytype] -inputs = ['avatar', 'cases'] +inputs = ['avatar'] [build-system] requires = ["flit_core==3.7.1"] |