1#!/usr/bin/env python3 2# Copyright 2023 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 24import pw_cli.log 25 26try: 27 from rp2040_utils import device_detector 28except ImportError: 29 # Load from this directory if rp2040_utils is not available. 30 import device_detector # type: ignore 31 32_LOG = logging.getLogger('unit_test_server') 33 34DEFAULT_PORT = 34172 35 36_TEST_RUNNER_COMMAND = 'rp2040_unit_test_runner' 37_TEST_SERVER_COMMAND = 'pw_target_runner_server' 38 39 40def parse_args(): 41 """Parses command-line arguments.""" 42 43 parser = argparse.ArgumentParser(description=__doc__) 44 parser.add_argument( 45 '--server-port', 46 type=int, 47 default=DEFAULT_PORT, 48 help='Port to launch the pw_target_runner_server on', 49 ) 50 parser.add_argument( 51 '--server-config', 52 type=argparse.FileType('r'), 53 help='Path to server config file', 54 ) 55 parser.add_argument( 56 '--debug-probe-only', 57 action='store_true', 58 help='Only run tests on detected Pi Pico debug probes', 59 ) 60 parser.add_argument( 61 '--pico-only', 62 action='store_true', 63 help='Only run tests on detected Pi Pico boards (NOT debug probes)', 64 ) 65 parser.add_argument( 66 '--verbose', 67 '-v', 68 dest='verbose', 69 action='store_true', 70 help='Output additional logs as the script runs', 71 ) 72 73 return parser.parse_args() 74 75 76def generate_runner(command: str, arguments: list[str]) -> str: 77 """Generates a text-proto style pw_target_runner_server configuration.""" 78 # TODO(amontanez): Use a real proto library to generate this when we have 79 # one set up. 80 for i, arg in enumerate(arguments): 81 arguments[i] = f' args: "{arg}"' 82 runner = ['runner {', f' command:"{command}"'] 83 runner.extend(arguments) 84 runner.append('}\n') 85 return '\n'.join(runner) 86 87 88def generate_server_config( 89 include_picos: bool = True, include_debug_probes: bool = True 90) -> IO[bytes]: 91 """Returns a temporary generated file for use as the server config.""" 92 boards = device_detector.detect_boards( 93 include_picos=include_picos, 94 include_debug_probes=include_debug_probes, 95 ) 96 97 if not boards: 98 _LOG.critical('No attached boards detected') 99 sys.exit(1) 100 101 if ( 102 len({'b' if b.is_debug_probe() else 'p': True for b in boards}.keys()) 103 > 1 104 ): 105 _LOG.critical( 106 'Debug probes and picos both detected. Mixed device configurations ' 107 'are not supported. Please only connect Picos directly, or only ' 108 'connect debug probes! You can also use --pico-only or ' 109 '--debug-probe-only to filter attached devices.' 110 ) 111 sys.exit(1) 112 113 config_file = tempfile.NamedTemporaryFile() 114 _LOG.debug('Generating test server config at %s', config_file.name) 115 _LOG.debug('Found %d attached devices', len(boards)) 116 117 picotool_boards = [board for board in boards if not board.is_debug_probe()] 118 if len(picotool_boards) > 1: 119 # TODO: https://pwbug.dev/290245354 - Multi-device flashing doesn't work 120 # due to limitations of picotool. Limit to one device even if multiple 121 # are connected. 122 _LOG.warning( 123 'TODO: https://pwbug.dev/290245354 - Multiple non-debugprobe ' 124 ' boards attached. ' 125 'Disabling parallel testing.' 126 ) 127 boards = boards[:1] 128 129 for board in boards: 130 test_runner_args = [ 131 '--usb-bus', 132 str(board.bus), 133 '--usb-port', 134 str(board.port), 135 ] 136 config_file.write( 137 generate_runner(_TEST_RUNNER_COMMAND, test_runner_args).encode( 138 'utf-8' 139 ) 140 ) 141 config_file.flush() 142 return config_file 143 144 145def launch_server( 146 server_config: IO[bytes] | None, 147 server_port: int | None, 148 include_picos: bool, 149 include_debug_probes: bool, 150) -> int: 151 """Launch a device test server with the provided arguments.""" 152 if server_config is None: 153 # Auto-detect attached boards if no config is provided. 154 server_config = generate_server_config( 155 include_picos, include_debug_probes 156 ) 157 158 cmd = [_TEST_SERVER_COMMAND, '-config', server_config.name] 159 160 if server_port is not None: 161 cmd.extend(['-port', str(server_port)]) 162 163 return pw_cli.process.run(*cmd, log_output=True).returncode 164 165 166def main(): 167 """Launch a device test server with the provided arguments.""" 168 args = parse_args() 169 170 log_level = logging.DEBUG if args.verbose else logging.INFO 171 pw_cli.log.install(level=log_level) 172 173 if args.pico_only and args.debug_probe_only: 174 _LOG.critical('Cannot specify both --pico-only and --debug-probe-only') 175 sys.exit(1) 176 177 exit_code = launch_server( 178 args.server_config, 179 args.server_port, 180 not args.debug_probe_only, 181 not args.pico_only, 182 ) 183 sys.exit(exit_code) 184 185 186if __name__ == '__main__': 187 main() 188