1#!/usr/bin/env python3
2#
3# Copyright (C) 2018 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may not
6# use this file except in compliance with the License. You may obtain a copy of
7# the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations under
15# the License.
16"""This file is used to send relay devices commands.
17
18Usage:
19    See --help for more details.
20
21    relay_tool.py -c <acts_config> -tb <testbed_name> -rd <device_name> -cmd cmd
22        Runs the command cmd for the given device pulled from the ACTS config.
23
24    relay_tool.py -c <acts_config> -tb <testbed_name> -rd <device_name> -l
25        Lists all possible functions (as well as documentation) for the device.
26
27Examples:
28
29    # For a Bluetooth Relay Device:
30    $ relay_tool.py -c ... -tb ... -rd ... -cmd get_mac_address
31    <MAC_ADDRESS>
32    relay_tool.py -c ... -tb ... -rd ... -cmd enter_pairing_mode
33    # No output. Waits for enter_pairing_mode to complete.
34"""
35
36import argparse
37import json
38import re
39import sys
40import inspect
41
42from acts.controllers import relay_device_controller
43
44
45def get_relay_device(config_path, testbed_name, device_name):
46    """Returns the relay device specified by the arguments.
47
48    Args:
49        config_path: the path to the ACTS config.
50        testbed_name: the name of the testbed the device is a part of.
51        device_name: the name of the device within the testbed.
52
53    Returns:
54        The RelayDevice object.
55    """
56    with open(config_path) as config_file:
57        config = json.load(config_file)
58
59    relay_config = config['testbed'][testbed_name]['RelayDevice']
60    relay_devices = relay_device_controller.create(relay_config)
61
62    try:
63        return next(device for device in relay_devices
64                    if device.name == device_name)
65    except StopIteration:
66        # StopIteration is raised when no device is found.
67        all_device_names = [device.name for device in relay_devices]
68        print('Unable to find device with name "%s" in testbed "%s". Expected '
69              'any of %s.' % (device_name, testbed_name, all_device_names),
70              file=sys.stderr)
71        raise
72
73
74def print_docstring(relay_device, func_name):
75    """Prints the docstring of the specified function to stderr.
76
77    Note that the documentation will be printed as follows:
78
79        func_name:
80            Docstring information, indented with a minimum of 4 spaces.
81
82    Args:
83        relay_device: the RelayDevice to find a function on.
84        func_name: the function to pull the docstring from.
85    """
86    func = getattr(relay_device, func_name)
87    signature = inspect.signature(func)
88    docstring = func.__doc__
89    if docstring is None:
90        docstring = '    No docstring available.'
91    else:
92        # Make the indentation uniform.
93
94        min_line_indentation = sys.maxsize
95        # Skip the first line, because docstrings begin with 'One liner.\n',
96        # instead of an indentation.
97        for line in docstring.split('\n')[1:]:
98            index = 0
99            for index, char in enumerate(line):
100                if char != ' ':
101                    break
102            if index + 1 < min_line_indentation and index != 0:
103                min_line_indentation = index + 1
104
105        if min_line_indentation == sys.maxsize:
106            min_line_indentation = 0
107
108        min_indent = '\n' + ' ' * (min_line_indentation - 4)
109        docstring = ' ' * 4 + docstring.rstrip()
110        docstring = re.sub(min_indent, '\n', docstring,
111                           flags=re.MULTILINE)
112
113    print('%s%s: \n%s\n' % (func_name, str(signature), docstring),
114          file=sys.stderr)
115
116
117def main():
118    parser = argparse.ArgumentParser()
119    parser.add_argument('-c', '--config', type=str, required=True,
120                        help='The path to the config file.')
121    parser.add_argument('-tb', '--testbed', type=str, required=True,
122                        help='The testbed within the config file to use.')
123    parser.add_argument('-rd', '--relay_device', type=str, required=True,
124                        help='The name of the relay device to use.')
125    group = parser.add_mutually_exclusive_group()
126    group.add_argument('-cmd', '--command', type=str, nargs='+',
127                       help='The command to run on the relay device.')
128    group.add_argument('-l', '--list_commmands',
129                       action='store_true',
130                       help='lists all commands for the given device.')
131
132    args = parser.parse_args()
133    relay_device = get_relay_device(args.config, args.testbed,
134                                    args.relay_device)
135
136    func_names = [func_name
137                  for func_name in dir(relay_device)
138                  if (not func_name.startswith('_') and
139                      not func_name.endswith('__') and
140                      callable(getattr(relay_device, func_name)))]
141
142    if args.command:
143        if args.command[0] not in func_names:
144            print('Received command %s. Expected any of %s.' %
145                  (repr(args.command[0]), repr(func_names)), file=sys.stderr)
146        else:
147            # getattr should not be able to fail here.
148            func = getattr(relay_device, args.command[0])
149            try:
150                ret = func(*args.command[1:])
151                if ret is not None:
152                    print(ret)
153            except TypeError as e:
154                # The above call may raise TypeError if an incorrect number
155                # of args are passed.
156                if len(e.args) == 1 and func.__name__ in e.args[0]:
157                    # If calling the function raised the TypeError, log this
158                    # more informative message instead of the traceback.
159                    print('Incorrect number of args passed to command "%s".' %
160                          args.command[0], file=sys.stderr)
161                    print_docstring(relay_device, args.command[0])
162                else:
163                    raise
164
165    else:  # args.list_commands is set
166        print('Note: These commands are specific to the device given. '
167              'Some of these commands may not work on other devices.\n',
168              file=sys.stderr)
169        for func_name in func_names:
170            print_docstring(relay_device, func_name)
171        exit(1)
172
173
174if __name__ == '__main__':
175    main()
176