1# Copyright (C) 2024 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Helper functions. 15 16Facilitates the implementation of a new profile proxy or a PTS MMI. 17""" 18 19import functools 20import textwrap 21import unittest 22import re 23 24DOCSTRING_WIDTH = 80 - 8 # 80 cols - 8 indentation spaces 25 26 27def assert_description(f): 28 """Decorator which verifies the description of a PTS MMI implementation. 29 30 Asserts that the docstring of a function implementing a PTS MMI is the same 31 as the corresponding official MMI description. 32 33 Args: 34 f: function implementing a PTS MMI. 35 36 Raises: 37 AssertionError: the docstring of the function does not match the MMI 38 description. 39 """ 40 41 @functools.wraps(f) 42 def wrapper(*args, **kwargs): 43 description = textwrap.fill(kwargs['description'], DOCSTRING_WIDTH, replace_whitespace=False) 44 description = ('\n'.join(map(lambda line: line.rstrip(), description.split('\n')))).strip() 45 docstring = textwrap.dedent(f.__doc__ or '').strip() 46 47 if docstring != description: 48 print(f'Expected description of {f.__name__}:') 49 print(description) 50 51 # Generate AssertionError. 52 test = unittest.TestCase() 53 test.maxDiff = None 54 test.assertMultiLineEqual(docstring, description, f'description does not match with function docstring of' 55 f' {f.__name__}') 56 57 return f(*args, **kwargs) 58 59 return wrapper 60 61 62def match_description(f): 63 """Extracts parameters from PTS MMI descriptions. 64 65 Similar to assert_description, but treats the description as an (indented) 66 regex that can be used to extract named capture groups from the PTS command. 67 68 Args: 69 f: function implementing a PTS MMI. 70 71 Raises: 72 AssertionError: the docstring of the function does not match the MMI 73 description. 74 """ 75 76 def normalize(desc): 77 return re.sub('\s+', ' ', desc).strip() 78 79 docstring = normalize(textwrap.dedent(f.__doc__)) 80 regex = re.compile(docstring) 81 82 @functools.wraps(f) 83 def wrapper(*args, **kwargs): 84 description = normalize(kwargs['description']) 85 match = regex.fullmatch(description) 86 87 assert match is not None, f'description does not match with function docstring of {f.__name__}:\n{repr(description)}\n!=\n{repr(docstring)}' 88 89 return f(*args, **kwargs, **match.groupdict()) 90 91 return wrapper 92 93 94def format_function(mmi_name, mmi_description): 95 """Returns the base format of a function implementing a PTS MMI.""" 96 wrapped_description = textwrap.fill(mmi_description, DOCSTRING_WIDTH, replace_whitespace=False) 97 wrapped_description = '\n'.join(map(lambda line: line.rstrip(), wrapped_description.split('\n'))) 98 return (f'@assert_description\n' 99 f'def {mmi_name}(self, **kwargs):\n' 100 f' """\n' 101 f'{textwrap.indent(wrapped_description, " ")}\n' 102 f' """\n' 103 f'\n' 104 f' return "OK"\n') 105 106 107def format_proxy(profile, mmi_name, mmi_description): 108 """Returns the base format of a profile proxy including a given MMI.""" 109 wrapped_function = textwrap.indent(format_function(mmi_name, mmi_description), ' ') 110 return (f'from mmi2grpc._helpers import assert_description\n' 111 f'from mmi2grpc._proxy import ProfileProxy\n' 112 f'\n' 113 f'from pandora_experimental.{profile.lower()}_grpc import {profile}\n' 114 f'\n' 115 f'\n' 116 f'class {profile}Proxy(ProfileProxy):\n' 117 f'\n' 118 f' def __init__(self, channel):\n' 119 f' super().__init__(channel)\n' 120 f' self.{profile.lower()} = {profile}(channel)\n' 121 f'\n' 122 f'{wrapped_function}') 123