xref: /aosp_15_r20/external/pigweed/pw_arduino_build/py/pw_arduino_build/unit_test_server.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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