1#!/usr/bin/env python3 2# Copyright 2019 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 26from stm32f429i_disc1_utils import stm32f429i_detector 27 28_LOG = logging.getLogger('unit_test_server') 29 30_TEST_RUNNER_COMMAND = 'stm32f429i_disc1_unit_test_runner' 31 32_TEST_SERVER_COMMAND = 'pw_target_runner_server' 33 34 35def parse_args(): 36 """Parses command-line arguments.""" 37 38 parser = argparse.ArgumentParser(description=__doc__) 39 parser.add_argument( 40 '--server-port', 41 type=int, 42 default=8080, 43 help='Port to launch the pw_target_runner_server on', 44 ) 45 parser.add_argument( 46 '--server-config', 47 type=argparse.FileType('r'), 48 help='Path to server config file', 49 ) 50 parser.add_argument( 51 '--verbose', 52 '-v', 53 dest='verbose', 54 action="store_true", 55 help='Output additional logs as the script runs', 56 ) 57 58 return parser.parse_args() 59 60 61def generate_runner(command: str, arguments: list[str]) -> str: 62 """Generates a text-proto style pw_target_runner_server configuration.""" 63 # TODO(amontanez): Use a real proto library to generate this when we have 64 # one set up. 65 for i, arg in enumerate(arguments): 66 arguments[i] = f' args: "{arg}"' 67 runner = ['runner {', f' command:"{command}"'] 68 runner.extend(arguments) 69 runner.append('}\n') 70 return '\n'.join(runner) 71 72 73def generate_server_config() -> IO[bytes]: 74 """Returns a temporary generated file for use as the server config.""" 75 boards = stm32f429i_detector.detect_boards() 76 if not boards: 77 _LOG.critical('No attached boards detected') 78 sys.exit(1) 79 config_file = tempfile.NamedTemporaryFile() 80 _LOG.debug('Generating test server config at %s', config_file.name) 81 _LOG.debug('Found %d attached devices', len(boards)) 82 for board in boards: 83 test_runner_args = [ 84 '--stlink-serial', 85 board.serial_number, 86 '--port', 87 board.dev_name, 88 ] 89 config_file.write( 90 generate_runner(_TEST_RUNNER_COMMAND, test_runner_args).encode( 91 'utf-8' 92 ) 93 ) 94 config_file.flush() 95 return config_file 96 97 98def launch_server( 99 server_config: IO[bytes] | None, server_port: int | None 100) -> int: 101 """Launch a device test server with the provided arguments.""" 102 if server_config is None: 103 # Auto-detect attached boards if no config is provided. 104 server_config = generate_server_config() 105 106 cmd = [_TEST_SERVER_COMMAND, '-config', server_config.name] 107 108 if server_port is not None: 109 cmd.extend(['-port', str(server_port)]) 110 111 return pw_cli.process.run(*cmd, log_output=True).returncode 112 113 114def main(): 115 """Launch a device test server with the provided arguments.""" 116 args = parse_args() 117 118 # Try to use pw_cli logs, else default to something reasonable. 119 pw_cli.log.install() 120 if args.verbose: 121 _LOG.setLevel(logging.DEBUG) 122 123 exit_code = launch_server(args.server_config, args.server_port) 124 sys.exit(exit_code) 125 126 127if __name__ == '__main__': 128 main() 129