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 Diff line number Diff line
@@ -36,6 +36,6 @@ filegroup {
    srcs: [
        "mmi2grpc/*.py",
        "pandora/*.py",
        ":pandora-grpc-python",
        ":pandora_experimental-python-src",
    ],
}
+21 −8
Original line number Diff line number Diff line
@@ -55,6 +55,19 @@ def generate_method(imports, file, service, method):
    output_type = import_type(imports, method.output_type)

    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 (
                f'def {method.name}(self, iterator, **kwargs):\n'
                f'    return self.channel.{input_mode}_{output_mode}(\n'
+12 −6
Original line number Diff line number Diff line
@@ -26,11 +26,12 @@ from mmi2grpc.a2dp import A2DPProxy
from mmi2grpc.avrcp import AVRCPProxy
from mmi2grpc.gatt import GATTProxy
from mmi2grpc.hfp import HFPProxy
from mmi2grpc.hogp import HOGPProxy
from mmi2grpc.sdp import SDPProxy
from mmi2grpc.sm import SMProxy
from mmi2grpc._helpers import format_proxy

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

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

    def __enter__(self):
        """Resets the IUT when starting a PTS test."""
        # Note: we don't keep a single gRPC channel instance in the IUT class
        # because reset is allowed to close the gRPC server.
        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):
        self._a2dp = None
@@ -77,6 +79,7 @@ class IUT:
        self._hfp = None
        self._sdp = None
        self._sm = None
        self._hogp = None

    def _retry(self, func):

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

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


    def interact(self, pts_address: bytes, profile: str, test: str, interaction: str, description: str, style: str,
                 **kwargs) -> str:
        """Routes MMI calls to corresponding profile proxy.
@@ -163,6 +164,11 @@ class IUT:
            if not self._sm:
                self._sm = SMProxy(grpc.insecure_channel(f'localhost:{self.port}'))
            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.
        code = format_proxy(profile, interaction, description)
+61 −34
Original line number Diff line number Diff line
@@ -11,7 +11,6 @@
# 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.

"""Helper functions.

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 textwrap
import unittest
import re

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
            description.
    """

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

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

        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


def format_function(mmi_name, mmi_description):
    """Returns the base format of a function implementing a PTS MMI."""
    wrapped_description = textwrap.fill(
        mmi_description, DOCSTRING_WIDTH, replace_whitespace=False)
    return (
        f'@assert_description\n'
    wrapped_description = textwrap.fill(mmi_description, DOCSTRING_WIDTH, replace_whitespace=False)
    return (f'@assert_description\n'
            f'def {mmi_name}(self, **kwargs):\n'
            f'    """\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):
    """Returns the base format of a profile proxy including a given MMI."""
    wrapped_function = textwrap.indent(
        format_function(mmi_name, mmi_description), '    ')
    return (
        f'from mmi2grpc._helpers import assert_description\n'
    wrapped_function = textwrap.indent(format_function(mmi_name, mmi_description), '    ')
    return (f'from mmi2grpc._helpers import assert_description\n'
            f'from mmi2grpc._proxy import ProfileProxy\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'class {profile}Proxy(ProfileProxy):\n'
+46 −0
Original line number 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