Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d8515045 authored by Charlie Boutier's avatar Charlie Boutier Committed by Android (Google) Code Review
Browse files

Merge changes from topic "update_pandora" into tm-qpr-dev

* changes:
  [Pandora] Don't bond when reconnecting to classic device
  avatar: Use pandora_experimental grpc interfaces
  Pandora: Rename copy of pandora interfaces to pandora_experimental
  [Pandora] Add PTS-Bot GATT tests GATT/CL/GAW
  [Pandora] Adopt permission from the shell instead of the platform certificate
  [Pandora] Fix HOGP regex
  PandoraServer: Replace usage of shutdownNow with shutdown
  PandoraServer: Avoid completing grpcUnary responseObserver twice
  PandoraServer: Close HFP Profile Proxy
  [PANDORA_TEST] Use new pairing interface in SMP
  [Pandora] Implement HID-over-GATT tests
  [Pandora] Implementation of Pandora Pairing interface
  [Pandora] Move pairing interface into a separate interface
  [Pandora] Utilities for bidirectional streaming APIs
  [PTS-Bot] Add 7 AVCTP test cases AVCTP/TG/CCM/BV-03-C AVCTP/TG/CCM/BV-04-C AVCTP/CT/CCM/BV-02-C AVCTP/CT/CCM/BV-03-C AVCTP/TG/NFR/BV-03-C AVCTP/TG/NFR/BI-01-C AVCTP/TG/FRA/BV-03-C
  PTS-bot: Skip flaky tests
  [Pandora] - Add SoftReset in sm.py
  Pandora: Add ResetBluetooth to Host
  [PANDORA_TEST] Add flaky SM test to skip list
  pandora: Add first basic avatar test
parents 52c168a5 f1728940
Loading
Loading
Loading
Loading
+1 −1
Original line number Original line Diff line number Diff line
@@ -36,6 +36,6 @@ filegroup {
    srcs: [
    srcs: [
        "mmi2grpc/*.py",
        "mmi2grpc/*.py",
        "pandora/*.py",
        "pandora/*.py",
        ":pandora-grpc-python",
        ":pandora_experimental-python-src",
    ],
    ],
}
}
+21 −8
Original line number Original line Diff line number Diff line
@@ -55,6 +55,19 @@ def generate_method(imports, file, service, method):
    output_type = import_type(imports, method.output_type)
    output_type = import_type(imports, method.output_type)


    if input_mode == 'stream':
    if input_mode == 'stream':
        if output_mode == 'stream':
            return (
                f'def {method.name}(self):\n'
                f'    from mmi2grpc._streaming import StreamWrapper\n'
                f'    return StreamWrapper(\n'
                f'        self.channel.{input_mode}_{output_mode}(\n'
                f"            '/{file.package}.{service.name}/{method.name}',\n"
                f'            request_serializer={input_type}.SerializeToString,\n'
                f'            response_deserializer={output_type}.FromString\n'
                f'        ),\n'
                f'        {input_type})'
            ).split('\n')
        else:
            return (
            return (
                f'def {method.name}(self, iterator, **kwargs):\n'
                f'def {method.name}(self, iterator, **kwargs):\n'
                f'    return self.channel.{input_mode}_{output_mode}(\n'
                f'    return self.channel.{input_mode}_{output_mode}(\n'
+12 −6
Original line number Original line Diff line number Diff line
@@ -26,11 +26,12 @@ from mmi2grpc.a2dp import A2DPProxy
from mmi2grpc.avrcp import AVRCPProxy
from mmi2grpc.avrcp import AVRCPProxy
from mmi2grpc.gatt import GATTProxy
from mmi2grpc.gatt import GATTProxy
from mmi2grpc.hfp import HFPProxy
from mmi2grpc.hfp import HFPProxy
from mmi2grpc.hogp import HOGPProxy
from mmi2grpc.sdp import SDPProxy
from mmi2grpc.sdp import SDPProxy
from mmi2grpc.sm import SMProxy
from mmi2grpc.sm import SMProxy
from mmi2grpc._helpers import format_proxy
from mmi2grpc._helpers import format_proxy


from pandora.host_grpc import Host
from pandora_experimental.host_grpc import Host


GRPC_PORT = 8999
GRPC_PORT = 8999
MAX_RETRIES = 10
MAX_RETRIES = 10
@@ -62,13 +63,14 @@ class IUT:
        self._hfp = None
        self._hfp = None
        self._sdp = None
        self._sdp = None
        self._sm = None
        self._sm = None
        self._hogp = None


    def __enter__(self):
    def __enter__(self):
        """Resets the IUT when starting a PTS test."""
        """Resets the IUT when starting a PTS test."""
        # Note: we don't keep a single gRPC channel instance in the IUT class
        # Note: we don't keep a single gRPC channel instance in the IUT class
        # because reset is allowed to close the gRPC server.
        # because reset is allowed to close the gRPC server.
        with grpc.insecure_channel(f'localhost:{self.port}') as channel:
        with grpc.insecure_channel(f'localhost:{self.port}') as channel:
            self._retry(Host(channel).Reset)(wait_for_ready=True)
            self._retry(Host(channel).HardReset)(wait_for_ready=True)


    def __exit__(self, exc_type, exc_value, exc_traceback):
    def __exit__(self, exc_type, exc_value, exc_traceback):
        self._a2dp = None
        self._a2dp = None
@@ -77,6 +79,7 @@ class IUT:
        self._hfp = None
        self._hfp = None
        self._sdp = None
        self._sdp = None
        self._sm = None
        self._sm = None
        self._hogp = None


    def _retry(self, func):
    def _retry(self, func):


@@ -106,8 +109,7 @@ class IUT:
        def read_local_address():
        def read_local_address():
            with grpc.insecure_channel(f'localhost:{self.port}') as channel:
            with grpc.insecure_channel(f'localhost:{self.port}') as channel:
                nonlocal mut_address
                nonlocal mut_address
                mut_address = self._retry(
                mut_address = self._retry(Host(channel).ReadLocalAddress)(wait_for_ready=True).address
                    Host(channel).ReadLocalAddress)(wait_for_ready=True).address


        thread = Thread(target=read_local_address)
        thread = Thread(target=read_local_address)
        thread.start()
        thread.start()
@@ -118,7 +120,6 @@ class IUT:
        else:
        else:
            return mut_address
            return mut_address



    def interact(self, pts_address: bytes, profile: str, test: str, interaction: str, description: str, style: str,
    def interact(self, pts_address: bytes, profile: str, test: str, interaction: str, description: str, style: str,
                 **kwargs) -> str:
                 **kwargs) -> str:
        """Routes MMI calls to corresponding profile proxy.
        """Routes MMI calls to corresponding profile proxy.
@@ -163,6 +164,11 @@ class IUT:
            if not self._sm:
            if not self._sm:
                self._sm = SMProxy(grpc.insecure_channel(f'localhost:{self.port}'))
                self._sm = SMProxy(grpc.insecure_channel(f'localhost:{self.port}'))
            return self._sm.interact(test, interaction, description, pts_address)
            return self._sm.interact(test, interaction, description, pts_address)
        # Handles HOGP MMIs.
        if profile in ('HOGP'):
            if not self._hogp:
                self._hogp = HOGPProxy(grpc.insecure_channel(f'localhost:{self.port}'))
            return self._hogp.interact(test, interaction, description, pts_address)


        # Handles unsupported profiles.
        # Handles unsupported profiles.
        code = format_proxy(profile, interaction, description)
        code = format_proxy(profile, interaction, description)
+61 −34
Original line number Original line Diff line number Diff line
@@ -11,7 +11,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# See the License for the specific language governing permissions and
# limitations under the License.
# limitations under the License.

"""Helper functions.
"""Helper functions.


Facilitates the implementation of a new profile proxy or a PTS MMI.
Facilitates the implementation of a new profile proxy or a PTS MMI.
@@ -20,6 +19,7 @@ Facilitates the implementation of a new profile proxy or a PTS MMI.
import functools
import functools
import textwrap
import textwrap
import unittest
import unittest
import re


DOCSTRING_WIDTH = 80 - 8  # 80 cols - 8 indentation spaces
DOCSTRING_WIDTH = 80 - 8  # 80 cols - 8 indentation spaces


@@ -37,10 +37,10 @@ def assert_description(f):
        AssertionError: the docstring of the function does not match the MMI
        AssertionError: the docstring of the function does not match the MMI
            description.
            description.
    """
    """

    @functools.wraps(f)
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
    def wrapper(*args, **kwargs):
        description = textwrap.fill(
        description = textwrap.fill(kwargs['description'], DOCSTRING_WIDTH, replace_whitespace=False)
            kwargs['description'], DOCSTRING_WIDTH, replace_whitespace=False)
        docstring = textwrap.dedent(f.__doc__ or '')
        docstring = textwrap.dedent(f.__doc__ or '')


        if docstring.strip() != description.strip():
        if docstring.strip() != description.strip():
@@ -50,22 +50,51 @@ def assert_description(f):
            # Generate AssertionError.
            # Generate AssertionError.
            test = unittest.TestCase()
            test = unittest.TestCase()
            test.maxDiff = None
            test.maxDiff = None
            test.assertMultiLineEqual(
            test.assertMultiLineEqual(docstring.strip(), description.strip(),
                docstring.strip(),
                description.strip(),
                                      f'description does not match with function docstring of'
                                      f'description does not match with function docstring of'
                                      f'{f.__name__}')
                                      f'{f.__name__}')


        return f(*args, **kwargs)
        return f(*args, **kwargs)

    return wrapper


def match_description(f):
    """Extracts parameters from PTS MMI descriptions.

    Similar to assert_description, but treats the description as an (indented)
    regex that can be used to extract named capture groups from the PTS command.

    Args:
        f: function implementing a PTS MMI.

    Raises:
        AssertionError: the docstring of the function does not match the MMI
            description.
    """

    def normalize(desc):
        return desc.replace("\n", " ").replace("\t", "    ").strip()

    docstring = normalize(textwrap.dedent(f.__doc__))
    regex = re.compile(docstring)

    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        description = normalize(kwargs['description'])
        match = regex.fullmatch(description)

        assert match is not None, f'description does not match with function docstring of {f.__name__}:\n{repr(description)}\n!=\n{repr(docstring)}'

        return f(*args, **kwargs, **match.groupdict())

    return wrapper
    return wrapper




def format_function(mmi_name, mmi_description):
def format_function(mmi_name, mmi_description):
    """Returns the base format of a function implementing a PTS MMI."""
    """Returns the base format of a function implementing a PTS MMI."""
    wrapped_description = textwrap.fill(
    wrapped_description = textwrap.fill(mmi_description, DOCSTRING_WIDTH, replace_whitespace=False)
        mmi_description, DOCSTRING_WIDTH, replace_whitespace=False)
    return (f'@assert_description\n'
    return (
        f'@assert_description\n'
            f'def {mmi_name}(self, **kwargs):\n'
            f'def {mmi_name}(self, **kwargs):\n'
            f'    """\n'
            f'    """\n'
            f'{textwrap.indent(wrapped_description, "    ")}\n'
            f'{textwrap.indent(wrapped_description, "    ")}\n'
@@ -76,13 +105,11 @@ def format_function(mmi_name, mmi_description):


def format_proxy(profile, mmi_name, mmi_description):
def format_proxy(profile, mmi_name, mmi_description):
    """Returns the base format of a profile proxy including a given MMI."""
    """Returns the base format of a profile proxy including a given MMI."""
    wrapped_function = textwrap.indent(
    wrapped_function = textwrap.indent(format_function(mmi_name, mmi_description), '    ')
        format_function(mmi_name, mmi_description), '    ')
    return (f'from mmi2grpc._helpers import assert_description\n'
    return (
        f'from mmi2grpc._helpers import assert_description\n'
            f'from mmi2grpc._proxy import ProfileProxy\n'
            f'from mmi2grpc._proxy import ProfileProxy\n'
            f'\n'
            f'\n'
        f'from pandora.{profile.lower()}_grpc import {profile}\n'
            f'from pandora_experimental.{profile.lower()}_grpc import {profile}\n'
            f'\n'
            f'\n'
            f'\n'
            f'\n'
            f'class {profile}Proxy(ProfileProxy):\n'
            f'class {profile}Proxy(ProfileProxy):\n'
+46 −0
Original line number Original line Diff line number Diff line
import queue


class IterableQueue:
    CLOSE = object()

    def __init__(self):
        self.queue = queue.Queue()

    def __iter__(self):
        return iter(self.queue.get, self.CLOSE)

    def put(self, value):
        self.queue.put(value)

    def close(self):
        self.put(self.CLOSE)


class StreamWrapper:

    def __init__(self, stream, ctor):
        self.tx_queue = IterableQueue()
        self.ctor = ctor

        # tx_queue is consumed on a separate thread, so
        # we don't block here
        self.rx_iter = stream(iter(self.tx_queue))

    def send(self, **kwargs):
        self.tx_queue.put(self.ctor(**kwargs))

    def __iter__(self):
        for value in self.rx_iter:
            yield value
        self.tx_queue.close()

    def recv(self):
        try:
            return next(self.rx_iter)
        except StopIteration:
            self.tx_queue.close()
            return

    def close(self):
        self.tx_queue.close()
Loading