• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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