1#!/usr/bin/env python3 2# Copyright 2020 The Pigweed Authors 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not 5# use this file except in compliance with the License. You may obtain a copy of 6# the License at 7# 8# https://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations under 14# the License. 15"""Launch a pw_target_runner server to use for multi-device testing.""" 16 17import argparse 18import logging 19import sys 20import tempfile 21from typing import IO 22 23import pw_cli.process 24 25import pw_arduino_build.log 26from pw_arduino_build import teensy_detector 27from pw_arduino_build.file_operations import decode_file_json 28from pw_arduino_build.unit_test_runner import ArduinoCoreNotSupported 29 30_LOG = logging.getLogger('unit_test_server') 31 32_TEST_RUNNER_COMMAND = 'arduino_unit_test_runner' 33 34_TEST_SERVER_COMMAND = 'pw_target_runner_server' 35 36 37class UnknownArduinoCore(Exception): 38 """Exception raised when no Arduino core can be found.""" 39 40 41def parse_args(): 42 """Parses command-line arguments.""" 43 44 parser = argparse.ArgumentParser(description=__doc__) 45 parser.add_argument( 46 '--server-port', 47 type=int, 48 default=8081, 49 help='Port to launch the pw_target_runner_server on', 50 ) 51 parser.add_argument( 52 '--server-config', 53 type=argparse.FileType('r'), 54 help='Path to server config file', 55 ) 56 parser.add_argument( 57 '--verbose', 58 '-v', 59 dest='verbose', 60 action="store_true", 61 help='Output additional logs as the script runs', 62 ) 63 parser.add_argument( 64 "-c", 65 "--config-file", 66 required=True, 67 help="Path to an arduino_builder config file.", 68 ) 69 # TODO(tonymd): Explicitly split args using "--". See example in: 70 # //pw_unit_test/py/pw_unit_test/test_runner.py:326 71 parser.add_argument( 72 'runner_args', 73 nargs=argparse.REMAINDER, 74 help='Arguments to forward to the test runner', 75 ) 76 77 return parser.parse_args() 78 79 80def generate_runner(command: str, arguments: list[str]) -> str: 81 """Generates a text-proto style pw_target_runner_server configuration.""" 82 # TODO(amontanez): Use a real proto library to generate this when we have 83 # one set up. 84 for i, arg in enumerate(arguments): 85 arguments[i] = f' args: "{arg}"' 86 runner = ['runner {', f' command:"{command}"'] 87 runner.extend(arguments) 88 runner.append('}\n') 89 return '\n'.join(runner) 90 91 92def generate_server_config( 93 runner_args: list[str] | None, arduino_package_path: str 94) -> IO[bytes]: 95 """Returns a temporary generated file for use as the server config.""" 96 97 if "teensy" not in arduino_package_path: 98 raise ArduinoCoreNotSupported(arduino_package_path) 99 100 boards = teensy_detector.detect_boards(arduino_package_path) 101 if not boards: 102 _LOG.critical('No attached boards detected') 103 sys.exit(1) 104 config_file = tempfile.NamedTemporaryFile() 105 _LOG.debug('Generating test server config at %s', config_file.name) 106 _LOG.debug('Found %d attached devices', len(boards)) 107 for board in boards: 108 test_runner_args = [] 109 if runner_args: 110 test_runner_args += runner_args 111 test_runner_args += ["-v"] + board.test_runner_args() 112 test_runner_args += ["--port", board.dev_name] 113 test_runner_args += ["--upload-tool", board.arduino_upload_tool_name] 114 config_file.write( 115 generate_runner(_TEST_RUNNER_COMMAND, test_runner_args).encode( 116 'utf-8' 117 ) 118 ) 119 config_file.flush() 120 return config_file 121 122 123def launch_server( 124 server_config: IO[bytes] | None, 125 server_port: int | None, 126 runner_args: list[str] | None, 127 arduino_package_path: str, 128) -> int: 129 """Launch a device test server with the provided arguments.""" 130 if server_config is None: 131 # Auto-detect attached boards if no config is provided. 132 server_config = generate_server_config( 133 runner_args, arduino_package_path 134 ) 135 136 cmd = [_TEST_SERVER_COMMAND, '-config', server_config.name] 137 138 if server_port is not None: 139 cmd.extend(['-port', str(server_port)]) 140 141 return pw_cli.process.run(*cmd, log_output=True).returncode 142 143 144def main(): 145 """Launch a device test server with the provided arguments.""" 146 args = parse_args() 147 148 if "--" in args.runner_args: 149 args.runner_args.remove("--") 150 151 log_level = logging.DEBUG if args.verbose else logging.INFO 152 pw_arduino_build.log.install(log_level) 153 154 # Get arduino_package_path from either the config file or command line args. 155 arduino_package_path = None 156 if args.config_file: 157 json_file_options, unused_config_path = decode_file_json( 158 args.config_file 159 ) 160 arduino_package_path = json_file_options.get( 161 "arduino_package_path", None 162 ) 163 # Must pass --config-file option in the runner_args. 164 if "--config-file" not in args.runner_args: 165 args.runner_args.append("--config-file") 166 args.runner_args.append(args.config_file) 167 168 # Check for arduino_package_path in the runner_args 169 try: 170 arduino_package_path = args.runner_args[ 171 args.runner_args.index("--arduino-package-path") + 1 172 ] 173 except (ValueError, IndexError): 174 # Only raise an error if arduino_package_path not set from the json. 175 if arduino_package_path is None: 176 raise UnknownArduinoCore( 177 "Test runner arguments: '{}'".format(" ".join(args.runner_args)) 178 ) 179 180 exit_code = launch_server( 181 args.server_config, 182 args.server_port, 183 args.runner_args, 184 arduino_package_path, 185 ) 186 sys.exit(exit_code) 187 188 189if __name__ == '__main__': 190 main() 191