aboutsummaryrefslogtreecommitdiff
path: root/pw_arduino_build
diff options
context:
space:
mode:
authorAnthony DiGirolamo <tonymd@google.com>2020-09-29 14:34:27 -0700
committerCQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>2020-10-12 19:02:19 +0000
commitc36af656df3632422c4cc892f1e8f9728def5a86 (patch)
treec51aae510bb3371ada5af878fbb77b8bb01d9a4e /pw_arduino_build
parent13c7c4fdaf42c09d70ef36e8c28c8356d4a16fb4 (diff)
downloadpigweed-c36af656df3632422c4cc892f1e8f9728def5a86.tar.gz
Arduino: unit_test_runner
- New unit test scripts - arduino_unit_test_runner - arduino_test_server - arduino_test_client - teensy_detector - arduino_builder changes - '--config-file' loading and '--save-config' options - '--set-variable' option to set arduino recipe vars. This is used for passing vars to the teensyloader flash recipe. - GN changes - exec_script(arduino_builder) calls will save config files to: ./out/arduino_debug/gen/arduino_builder_config.json - 'pw_arduino_use_test_server' gn build arg The test_runner and test_server both require a options to run properly, see targets/arduino/target_docs.rst for usage. Change-Id: I118498c291d2fbb034faa372b9250f6a0783a478 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/19220 Commit-Queue: Anthony DiGirolamo <tonymd@google.com> Reviewed-by: Armando Montanez <amontanez@google.com>
Diffstat (limited to 'pw_arduino_build')
-rw-r--r--pw_arduino_build/arduino.gni13
-rw-r--r--pw_arduino_build/py/pw_arduino_build/__main__.py190
-rwxr-xr-xpw_arduino_build/py/pw_arduino_build/builder.py59
-rw-r--r--pw_arduino_build/py/pw_arduino_build/core_installer.py11
-rw-r--r--pw_arduino_build/py/pw_arduino_build/file_operations.py40
-rw-r--r--pw_arduino_build/py/pw_arduino_build/log.py24
-rw-r--r--pw_arduino_build/py/pw_arduino_build/teensy_detector.py114
-rwxr-xr-xpw_arduino_build/py/pw_arduino_build/unit_test_client.py58
-rwxr-xr-xpw_arduino_build/py/pw_arduino_build/unit_test_runner.py340
-rw-r--r--pw_arduino_build/py/pw_arduino_build/unit_test_server.py164
-rw-r--r--pw_arduino_build/py/setup.py12
11 files changed, 929 insertions, 96 deletions
diff --git a/pw_arduino_build/arduino.gni b/pw_arduino_build/arduino.gni
index c21463251..8c89202cd 100644
--- a/pw_arduino_build/arduino.gni
+++ b/pw_arduino_build/arduino.gni
@@ -15,6 +15,10 @@
# gn-format disable
import("//build_overrides/pigweed.gni")
declare_args() {
+ # Enable/disable Arduino builds via group("arduino").
+ # Set to the full path of ./third_party/arduino
+ dir_pw_third_party_arduino = ""
+
# Expected args for an Arduino build:
arduino_core_name = "teensy"
arduino_package_name = "teensy/avr"
@@ -25,10 +29,6 @@ declare_args() {
"menu.usb.serial",
"menu.keys.en-us",
]
-
- # Enable/disable Arduino builds via group("arduino").
- # Set to the full path of ./third_party/arduino
- dir_pw_third_party_arduino = ""
}
arduino_builder_script =
@@ -46,6 +46,11 @@ arduino_global_args = [
arduino_package_name,
"--compiler-path-override",
_compiler_path_override,
+
+ # Save config files to "out/arduino_debug/gen/arduino_builder_config.json"
+ "--config-file",
+ rebase_path(root_gen_dir) + "/arduino_builder_config.json",
+ "--save-config",
]
arduino_board_args = [
diff --git a/pw_arduino_build/py/pw_arduino_build/__main__.py b/pw_arduino_build/py/pw_arduino_build/__main__.py
index 0bdbcf124..16dfde796 100644
--- a/pw_arduino_build/py/pw_arduino_build/__main__.py
+++ b/pw_arduino_build/py/pw_arduino_build/__main__.py
@@ -15,7 +15,7 @@
"""Command line interface for arduino_builder."""
import argparse
-import glob
+import json
import logging
import os
import pprint
@@ -26,6 +26,7 @@ from typing import List
from pw_arduino_build import core_installer, log
from pw_arduino_build.builder import ArduinoBuilder
+from pw_arduino_build.file_operations import decode_file_json
_LOG = logging.getLogger(__name__)
@@ -33,6 +34,10 @@ _pretty_print = pprint.PrettyPrinter(indent=1, width=120).pprint
_pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
+class MissingArduinoCore(Exception):
+ """Exception raised when an Arduino core can not be found."""
+
+
def list_boards_command(unused_args, builder):
# list-boards subcommand
# (does not need a selected board or default menu options)
@@ -65,8 +70,8 @@ def list_menu_options_command(args, builder):
print("\nDefault Options")
print(separator)
- default_options, unused_column_widths = builder.get_default_menu_options()
- for name, description in default_options:
+ menu_options, unused_col_widths = builder.get_default_menu_options()
+ for name, description in menu_options:
print(name.ljust(all_column_widths[0] + 1), description)
@@ -198,8 +203,11 @@ def show_command(args, builder):
elif args.core_path:
print(builder.get_core_path())
- elif args.postbuild:
- print(builder.get_postbuild_line(args.postbuild))
+ elif args.prebuilds:
+ show_command_print_string_list(args, builder.get_prebuild_steps())
+
+ elif args.postbuilds:
+ show_command_print_string_list(args, builder.get_postbuild_steps())
elif args.upload_command:
print(builder.get_upload_line(args.upload_command, args.serial_port))
@@ -243,14 +251,13 @@ def show_command(args, builder):
show_command_print_string_list(args, vfiles)
-def add_common_options(parser, serial_port, build_path, build_project_name,
- project_path, project_source_path):
+def add_common_parser_args(parser, serial_port, build_path, build_project_name,
+ project_path, project_source_path):
"""Add command line options common to the run and show commands."""
parser.add_argument(
"--serial-port",
default=serial_port,
- help="Serial port for flashing flash. Default: '{}'".format(
- serial_port))
+ help="Serial port for flashing. Default: '{}'".format(serial_port))
parser.add_argument(
"--build-path",
default=build_path,
@@ -272,7 +279,98 @@ def add_common_options(parser, serial_port, build_path, build_project_name,
help="Name of the board to use.")
# nargs="+" is one or more args, e.g:
# --menu-options menu.usb.serialhid menu.speed.150
- parser.add_argument("--menu-options", nargs="+", type=str)
+ parser.add_argument(
+ "--menu-options",
+ nargs="+",
+ type=str,
+ metavar="menu.usb.serial",
+ help="Desired Arduino menu options. See the "
+ "'list-menu-options' subcommand for available options.")
+ parser.add_argument("--set-variable",
+ action="append",
+ metavar='some.variable=NEW_VALUE',
+ help="Override an Arduino recipe variable. May be "
+ "specified multiple times. For example: "
+ "--set-variable 'serial.port.label=/dev/ttyACM0' "
+ "--set-variable 'serial.port.protocol=Teensy'")
+
+
+def check_for_missing_args(args):
+ if args.arduino_package_path is None:
+ raise MissingArduinoCore(
+ "Please specify the location of an Arduino core using "
+ "'--arduino-package-path' and '--arduino-package-name'.")
+
+
+# TODO(tonymd): These defaults don't make sense anymore and should be removed.
+def get_default_options():
+ defaults = {}
+ defaults["build_path"] = os.path.realpath(
+ os.path.expanduser(
+ os.path.expandvars(os.path.join(os.getcwd(), "build"))))
+ defaults["project_path"] = os.path.realpath(
+ os.path.expanduser(os.path.expandvars(os.getcwd())))
+ defaults["project_source_path"] = os.path.join(defaults["project_path"],
+ "src")
+ defaults["build_project_name"] = os.path.basename(defaults["project_path"])
+ defaults["serial_port"] = "UNKNOWN"
+ return defaults
+
+
+def load_config_file(args, default_options):
+ """Load a config file and merge with command line options.
+
+ Command line takes precedence over values loaded from a config file."""
+
+ if args.save_config and not args.config_file:
+ raise FileNotFoundError(
+ "'--save-config' requires the '--config-file' option")
+
+ if not args.config_file:
+ return
+
+ commandline_options = {
+ # Global option
+ "arduino_package_path": args.arduino_package_path,
+ "arduino_package_name": args.arduino_package_name,
+ "compiler_path_override": args.compiler_path_override,
+ # These options may not exist unless show or run command
+ "build_path": getattr(args, "build_path", None),
+ "project_path": getattr(args, "project_path", None),
+ "project_source_path": getattr(args, "project_source_path", None),
+ "build_project_name": getattr(args, "build_project_name", None),
+ "board": getattr(args, "board", None),
+ "menu_options": getattr(args, "menu_options", None),
+ }
+
+ # Decode JSON config file.
+ json_file_options, config_file_path = decode_file_json(args.config_file)
+
+ # Merge config file with command line options.
+ merged_options = {}
+ for key, value in commandline_options.items():
+ # Use the command line specified option by default
+ merged_options[key] = value
+
+ # Is this option in the config file?
+ if json_file_options.get(key, None) is not None:
+ # Use the json defined option if it's not set on the command
+ # line (or is a default value).
+ if value is None or value == default_options.get(key, None):
+ merged_options[key] = json_file_options[key]
+
+ # Update args namespace to matched merged_options.
+ for key, value in merged_options.items():
+ setattr(args, key, value)
+
+ # Write merged_options if --save-config.
+ if args.save_config:
+ encoded_json = json.dumps(merged_options, indent=4)
+ # Create parent directories
+ os.makedirs(os.path.dirname(config_file_path), exist_ok=True)
+ # Save json file.
+ with open(config_file_path, "w") as jfile:
+ jfile.write(encoded_json)
def main():
@@ -299,20 +397,7 @@ def main():
help='Set the log level '
'(debug, info, warning, error, critical)')
- build_path = os.path.realpath(
- os.path.expanduser(os.path.expandvars("./build")))
- project_path = os.path.realpath(
- os.path.expanduser(os.path.expandvars("./")))
- project_source_path = os.path.join(project_path, "src")
- build_project_name = os.path.basename(project_path)
-
- serial_port = "UNKNOWN"
- # TODO(tonymd): Temp solution to passing in serial port. It should use
- # arduino core discovery tools.
- possible_serial_ports = glob.glob("/dev/ttyACM*") + glob.glob(
- "/dev/ttyUSB*")
- if possible_serial_ports:
- serial_port = possible_serial_ports[0]
+ default_options = get_default_options()
# Global command line options
parser.add_argument("--arduino-package-path",
@@ -322,6 +407,10 @@ def main():
parser.add_argument("--compiler-path-override",
help="Path to arm-none-eabi-gcc bin folder. "
"Default: Arduino core specified gcc")
+ parser.add_argument("-c", "--config-file", help="Path to a config file.")
+ parser.add_argument("--save-config",
+ action="store_true",
+ help="Save command line arguments to the config file.")
# Subcommands
subparsers = parser.add_subparsers(title="subcommand",
@@ -360,8 +449,11 @@ def main():
# show command
show_parser = subparsers.add_parser("show",
help="Return compiler information.")
- add_common_options(show_parser, serial_port, build_path,
- build_project_name, project_path, project_source_path)
+ add_common_parser_args(show_parser, default_options["serial_port"],
+ default_options["build_path"],
+ default_options["build_project_name"],
+ default_options["project_path"],
+ default_options["project_source_path"])
show_parser.add_argument("--delimit-with-newlines",
help="Separate flag output with newlines.",
action="store_true")
@@ -385,8 +477,12 @@ def main():
output_group.add_argument("--ar-binary", action="store_true")
output_group.add_argument("--objcopy-binary", action="store_true")
output_group.add_argument("--size-binary", action="store_true")
- output_group.add_argument("--postbuild",
- help="Show recipe.hooks.postbuild.*.pattern")
+ output_group.add_argument("--prebuilds",
+ action="store_true",
+ help="Show prebuild step commands.")
+ output_group.add_argument("--postbuilds",
+ action="store_true",
+ help="Show postbuild step commands.")
output_group.add_argument("--upload-tools", action="store_true")
output_group.add_argument("--upload-command")
output_group.add_argument("--library-includes", action="store_true")
@@ -403,8 +499,11 @@ def main():
# run command
run_parser = subparsers.add_parser("run", help="Run Arduino recipes.")
- add_common_options(run_parser, serial_port, build_path, build_project_name,
- project_path, project_source_path)
+ add_common_parser_args(run_parser, default_options["serial_port"],
+ default_options["build_path"],
+ default_options["build_project_name"],
+ default_options["project_path"],
+ default_options["project_source_path"])
run_parser.add_argument("--run-link",
nargs="+",
type=str,
@@ -417,37 +516,44 @@ def main():
run_parser.set_defaults(func=run_command)
+ # Parse command line arguments.
args = parser.parse_args()
-
- log.install()
- log.set_level(args.loglevel)
-
_LOG.debug(_pretty_format(args))
+ log.install(args.loglevel)
+
# Check for and set alternate compiler path.
- compiler_path_override = False
if args.compiler_path_override:
+ # Get absolute path
compiler_path_override = os.path.realpath(
os.path.expanduser(os.path.expandvars(
args.compiler_path_override)))
+ args.compiler_path_override = compiler_path_override
+
+ load_config_file(args, default_options)
if args.subcommand == "install-core":
args.func(args)
elif args.subcommand in ["list-boards", "list-menu-options"]:
+ check_for_missing_args(args)
builder = ArduinoBuilder(args.arduino_package_path,
args.arduino_package_name)
builder.load_board_definitions()
args.func(args, builder)
else:
- builder = ArduinoBuilder(args.arduino_package_path,
- args.arduino_package_name,
- build_path=args.build_path,
- build_project_name=args.build_project_name,
- project_path=args.project_path,
- project_source_path=args.project_source_path,
- compiler_path_override=compiler_path_override)
+ check_for_missing_args(args)
+ builder = ArduinoBuilder(
+ args.arduino_package_path,
+ args.arduino_package_name,
+ build_path=args.build_path,
+ build_project_name=args.build_project_name,
+ project_path=args.project_path,
+ project_source_path=args.project_source_path,
+ compiler_path_override=args.compiler_path_override)
builder.load_board_definitions()
builder.select_board(args.board, args.menu_options)
+ if args.set_variable:
+ builder.set_variables(args.set_variable)
args.func(args, builder)
sys.exit(0)
diff --git a/pw_arduino_build/py/pw_arduino_build/builder.py b/pw_arduino_build/py/pw_arduino_build/builder.py
index 06c4117cb..34d56e42f 100755
--- a/pw_arduino_build/py/pw_arduino_build/builder.py
+++ b/pw_arduino_build/py/pw_arduino_build/builder.py
@@ -46,27 +46,27 @@ class ArduinoBuilder:
"""Used to interpret arduino boards.txt and platform.txt files."""
# pylint: disable=too-many-instance-attributes,too-many-public-methods
- board_menu_regex = re.compile(
+ BOARD_MENU_REGEX = re.compile(
r"^(?P<name>menu\.[^#=]+)=(?P<description>.*)$", re.MULTILINE)
- board_name_regex = re.compile(
+ BOARD_NAME_REGEX = re.compile(
r"^(?P<name>[^\s#\.]+)\.name=(?P<description>.*)$", re.MULTILINE)
- variable_regex = re.compile(r"^(?P<name>[^\s#=]+)=(?P<value>.*)$",
+ VARIABLE_REGEX = re.compile(r"^(?P<name>[^\s#=]+)=(?P<value>.*)$",
re.MULTILINE)
- menu_option_regex = re.compile(
+ MENU_OPTION_REGEX = re.compile(
r"^menu\." # starts with "menu"
r"(?P<menu_option_name>[^.]+)\." # first token after .
r"(?P<menu_option_value>[^.]+)$") # second (final) token after .
- tool_name_regex = re.compile(
+ TOOL_NAME_REGEX = re.compile(
r"^tools\." # starts with "tools"
r"(?P<tool_name>[^.]+)\.") # first token after .
- interpolated_variable_regex = re.compile(r"{[^}]+}", re.MULTILINE)
+ INTERPOLATED_VARIABLE_REGEX = re.compile(r"{[^}]+}", re.MULTILINE)
- objcopy_step_name_regex = re.compile(r"^recipe.objcopy.([^.]+).pattern$")
+ OBJCOPY_STEP_NAME_REGEX = re.compile(r"^recipe.objcopy.([^.]+).pattern$")
def __init__(self,
arduino_path,
@@ -106,9 +106,9 @@ class ArduinoBuilder:
self.hardware_path = os.path.join(self.arduino_path, "hardware")
if not os.path.exists(self.hardware_path):
- _LOG.error("Error: Arduino package path '%s' does not exist.",
- self.arduino_path)
- raise FileNotFoundError
+ raise FileNotFoundError(
+ "Arduino package path '{}' does not exist.".format(
+ self.arduino_path))
# Set and check for valid package name
self.package_path = os.path.join(self.arduino_path, "hardware",
@@ -158,6 +158,18 @@ class ArduinoBuilder:
self._apply_recipe_overrides()
self._substitute_variables()
+ def set_variables(self, variable_list: List[str]):
+ # Convert the string list containing 'name=value' items into a dict
+ variable_source = {}
+ for var in variable_list:
+ var_name, value = var.split("=")
+ variable_source[var_name] = value
+
+ # Replace variables in platform
+ for var, value in self.platform.items():
+ self.platform[var] = self._replace_variables(
+ value, variable_source)
+
def _apply_recipe_overrides(self):
# Override link recipes with per-core exceptions
# Teensyduino cores
@@ -215,7 +227,7 @@ class ArduinoBuilder:
return False
# Override default menu option with new value.
- menu_match_result = self.menu_option_regex.match(moption)
+ menu_match_result = self.MENU_OPTION_REGEX.match(moption)
if menu_match_result:
menu_match = menu_match_result.groupdict()
menu_value = menu_match["menu_option_value"]
@@ -234,6 +246,9 @@ class ArduinoBuilder:
https://arduino.github.io/arduino-cli/platform-specification/#global-predefined-properties
"""
+ # TODO(tonymd): Make sure these variables are replaced in recipe lines
+ # even if they are None: build_path, project_path, project_source_path,
+ # build_project_name
for current_board_name in self.board.keys():
if self.build_path:
self.board[current_board_name]["build.path"] = self.build_path
@@ -301,7 +316,7 @@ class ArduinoBuilder:
# Load platform.txt
with open(self.platform_txt, "r") as pfile:
platform_file = pfile.read()
- platform_var_matches = self.variable_regex.finditer(platform_file)
+ platform_var_matches = self.VARIABLE_REGEX.finditer(platform_file)
for p_match in [m.groupdict() for m in platform_var_matches]:
self.platform[p_match["name"]] = p_match["value"]
@@ -309,14 +324,14 @@ class ArduinoBuilder:
with open(self.boards_txt, "r") as bfile:
board_file = bfile.read()
# Get all top-level menu options, e.g. menu.usb=USB Type
- board_menu_matches = self.board_menu_regex.finditer(board_file)
+ board_menu_matches = self.BOARD_MENU_REGEX.finditer(board_file)
for menuitem in [m.groupdict() for m in board_menu_matches]:
self.menu_options["global_options"][menuitem["name"]] = {
"description": menuitem["description"]
}
# Get all board names, e.g. teensy40.name=Teensy 4.0
- board_name_matches = self.board_name_regex.finditer(board_file)
+ board_name_matches = self.BOARD_NAME_REGEX.finditer(board_file)
for b_match in [m.groupdict() for m in board_name_matches]:
self.board[b_match["name"]] = OrderedDict()
self.menu_options["default_board_values"][
@@ -377,7 +392,7 @@ class ArduinoBuilder:
definitions from variable_lookup_source.
"""
new_line = line
- for current_var_match in self.interpolated_variable_regex.findall(
+ for current_var_match in self.INTERPOLATED_VARIABLE_REGEX.findall(
line):
# {build.flags.c} --> build.flags.c
current_var = current_var_match.strip("{}")
@@ -520,7 +535,7 @@ class ArduinoBuilder:
max_string_length = [0, 0]
for key_name, description in self.board[self.selected_board].items():
- menu_match_result = self.menu_option_regex.match(key_name)
+ menu_match_result = self.MENU_OPTION_REGEX.match(key_name)
if menu_match_result:
menu_match = menu_match_result.groupdict()
name = "menu.{}.{}".format(menu_match["menu_option_name"],
@@ -572,7 +587,7 @@ class ArduinoBuilder:
return line
def _get_tool_name(self, line):
- tool_match_result = self.tool_name_regex.match(line)
+ tool_match_result = self.TOOL_NAME_REGEX.match(line)
if tool_match_result:
return tool_match_result[1]
return False
@@ -580,7 +595,7 @@ class ArduinoBuilder:
def get_upload_tool_names(self):
return [
self._get_tool_name(t) for t in self.platform.keys()
- if self.tool_name_regex.match(t) and 'upload.pattern' in t
+ if self.TOOL_NAME_REGEX.match(t) and 'upload.pattern' in t
]
# TODO(tonymd): Use these getters in _replace_variables() or
@@ -595,7 +610,7 @@ class ArduinoBuilder:
line = self.platform.get(variable, False)
# Get all unique variables used in this line in line.
unique_vars = sorted(
- set(self.interpolated_variable_regex.findall(line)))
+ set(self.INTERPOLATED_VARIABLE_REGEX.findall(line)))
# Search for each unique_vars in namespace and global.
for var in unique_vars:
v_raw_name = var.strip("{}")
@@ -775,14 +790,14 @@ class ArduinoBuilder:
def get_objcopy_step_names(self):
names = [
name for name, line in self.platform.items()
- if self.objcopy_step_name_regex.match(name)
+ if self.OBJCOPY_STEP_NAME_REGEX.match(name)
]
return names
def get_objcopy_steps(self) -> List[str]:
lines = [
line for name, line in self.platform.items()
- if self.objcopy_step_name_regex.match(name)
+ if self.OBJCOPY_STEP_NAME_REGEX.match(name)
]
lines = [
self.replace_compile_binary_with_override_path(line)
@@ -802,7 +817,7 @@ class ArduinoBuilder:
objcopy_suffixes = [
m[1] for m in [
- self.objcopy_step_name_regex.match(line)
+ self.OBJCOPY_STEP_NAME_REGEX.match(line)
for line in objcopy_step_names
] if m
]
diff --git a/pw_arduino_build/py/pw_arduino_build/core_installer.py b/pw_arduino_build/py/pw_arduino_build/core_installer.py
index a1e3d3c74..35d630876 100644
--- a/pw_arduino_build/py/pw_arduino_build/core_installer.py
+++ b/pw_arduino_build/py/pw_arduino_build/core_installer.py
@@ -28,6 +28,11 @@ import pw_arduino_build.file_operations as file_operations
_LOG = logging.getLogger(__name__)
+
+class ArduinoCoreNotSupported(Exception):
+ """Exception raised when a given core can not be installed."""
+
+
# yapf: disable
_ARDUINO_CORE_ARTIFACTS: Dict[str, Dict] = {
# pylint: disable=line-too-long
@@ -169,9 +174,9 @@ def install_core_command(args: argparse.Namespace):
elif args.core_name == "arduino-samd":
install_arduino_samd_core(install_prefix, install_dir, cache_dir)
else:
- _LOG.error("Invalid core '%s'. Supported cores: %s", args.core_name,
- ", ".join(supported_cores()))
- sys.exit(1)
+ raise ArduinoCoreNotSupported(
+ "Invalid core '{}'. Supported cores: {}".format(
+ args.core_name, ", ".join(supported_cores())))
def supported_cores():
diff --git a/pw_arduino_build/py/pw_arduino_build/file_operations.py b/pw_arduino_build/py/pw_arduino_build/file_operations.py
index 2bcdb69ca..1e5796313 100644
--- a/pw_arduino_build/py/pw_arduino_build/file_operations.py
+++ b/pw_arduino_build/py/pw_arduino_build/file_operations.py
@@ -16,6 +16,7 @@
import glob
import hashlib
+import json
import logging
import os
import shutil
@@ -38,8 +39,8 @@ def find_files(starting_dir: str,
directories_only=False) -> List[str]:
original_working_dir = os.getcwd()
if not (os.path.exists(starting_dir) and os.path.isdir(starting_dir)):
- _LOG.error("Directory '%s' does not exist.", starting_dir)
- raise FileNotFoundError
+ raise FileNotFoundError(
+ "Directory '{}' does not exist.".format(starting_dir))
os.chdir(starting_dir)
files = []
@@ -72,17 +73,11 @@ def verify_file_checksum(file_path,
expected_checksum,
sum_function=sha256_sum):
downloaded_checksum = sum_function(file_path)
- try:
- if downloaded_checksum != expected_checksum:
- raise InvalidChecksumError
- except InvalidChecksumError:
- _LOG.exception("Invalid %s\n"
- "%s %s\n"
- "%s (expected)",
- sum_function.__name__, downloaded_checksum,
- os.path.basename(file_path), expected_checksum)
- # Exit to stop installation
- return sys.exit(1)
+ if downloaded_checksum != expected_checksum:
+ raise InvalidChecksumError(
+ f"Invalid {sum_function.__name__}\n"
+ f"{downloaded_checksum} {os.path.basename(file_path)}\n"
+ f"{expected_checksum} (expected)")
_LOG.info(" %s:", sum_function.__name__)
_LOG.info(" %s %s", downloaded_checksum, os.path.basename(file_path))
@@ -188,3 +183,22 @@ def remove_empty_directories(directory):
# if empty directory
elif path.is_dir() and len(os.listdir(path)) == 0:
path.rmdir()
+
+
+def decode_file_json(file_name):
+ """Decode JSON values from a file.
+
+ Does not raise an error if the file cannot be decoded."""
+
+ # Get absolute path to the file.
+ file_path = os.path.realpath(
+ os.path.expanduser(os.path.expandvars(file_name)))
+
+ json_file_options = {}
+ try:
+ with open(file_path, "r") as jfile:
+ json_file_options = json.loads(jfile.read())
+ except (FileNotFoundError, json.JSONDecodeError):
+ _LOG.warning("Unable to read file '%s'", file_path)
+
+ return json_file_options, file_path
diff --git a/pw_arduino_build/py/pw_arduino_build/log.py b/pw_arduino_build/py/pw_arduino_build/log.py
index d5ed9f795..d5afb3035 100644
--- a/pw_arduino_build/py/pw_arduino_build/log.py
+++ b/pw_arduino_build/py/pw_arduino_build/log.py
@@ -22,16 +22,20 @@ _STDERR_HANDLER = logging.StreamHandler()
def install(level: int = logging.INFO) -> None:
"""Configure the system logger for the arduino_builder log format."""
- # Set log level on root logger to debug, otherwise any higher levels
- # elsewhere are ignored.
- root = logging.getLogger()
- root.setLevel(logging.DEBUG)
-
- _STDERR_HANDLER.setLevel(level)
- _STDERR_HANDLER.setFormatter(
- logging.Formatter("[%(asctime)s] "
- "%(levelname)s %(message)s", "%Y%m%d %H:%M:%S"))
- root.addHandler(_STDERR_HANDLER)
+ try:
+ import pw_cli.log # pylint: disable=import-outside-toplevel
+ pw_cli.log.install(level=level)
+ except ImportError:
+ # Set log level on root logger to debug, otherwise any higher levels
+ # elsewhere are ignored.
+ root = logging.getLogger()
+ root.setLevel(logging.DEBUG)
+
+ _STDERR_HANDLER.setLevel(level)
+ _STDERR_HANDLER.setFormatter(
+ logging.Formatter("[%(asctime)s] "
+ "%(levelname)s %(message)s", "%Y%m%d %H:%M:%S"))
+ root.addHandler(_STDERR_HANDLER)
def set_level(log_level: int):
diff --git a/pw_arduino_build/py/pw_arduino_build/teensy_detector.py b/pw_arduino_build/py/pw_arduino_build/teensy_detector.py
new file mode 100644
index 000000000..7c6bdf4d9
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/teensy_detector.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Detects attached Teensy boards connected via usb."""
+
+import logging
+import os
+import re
+import subprocess
+import typing
+
+from typing import List
+
+import pw_arduino_build.log
+
+_LOG = logging.getLogger('teensy_detector')
+
+
+def log_subprocess_output(level, output):
+ """Logs subprocess output line-by-line."""
+
+ lines = output.decode('utf-8', errors='replace').splitlines()
+ for line in lines:
+ _LOG.log(level, line)
+
+
+class BoardInfo(typing.NamedTuple):
+ """Information about a connected dev board."""
+ dev_name: str
+ usb_device_path: str
+ protocol: str
+ label: str
+ arduino_upload_tool_name: str
+
+ def test_runner_args(self) -> List[str]:
+ return [
+ "--set-variable", f"serial.port.protocol={self.protocol}",
+ "--set-variable", f"serial.port={self.usb_device_path}",
+ "--set-variable", f"serial.port.label={self.dev_name}"
+ ]
+
+
+def detect_boards(arduino_package_path=False) -> list:
+ """Detect attached boards, returning a list of Board objects."""
+
+ if not arduino_package_path:
+ arduino_package_path = os.path.join("third_party", "arduino", "cores",
+ "teensy")
+
+ teensy_device_line_regex = re.compile(
+ r"^(?P<address>[^ ]+) (?P<dev_name>[^ ]+) "
+ r"\((?P<label>[^)]+)\) ?(?P<rest>.*)$")
+
+ boards = []
+ detect_command = [
+ os.path.join(os.getcwd(), arduino_package_path, "hardware", "tools",
+ "teensy_ports"), "-L"
+ ]
+
+ process = subprocess.run(detect_command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+ if process.returncode != 0:
+ _LOG.error("Command failed with exit code %d.", process.returncode)
+ _LOG.error("Full command:")
+ _LOG.error("")
+ _LOG.error(" %s", " ".join(detect_command))
+ _LOG.error("")
+ _LOG.error("Process output:")
+ log_subprocess_output(logging.ERROR, process.stdout)
+ _LOG.error('')
+ for line in process.stdout.decode("utf-8", errors="replace").splitlines():
+ device_match_result = teensy_device_line_regex.match(line)
+ if device_match_result:
+ teensy_device = device_match_result.groupdict()
+ boards.append(
+ BoardInfo(dev_name=teensy_device["dev_name"],
+ usb_device_path=teensy_device["address"],
+ protocol="Teensy",
+ label=teensy_device["label"],
+ arduino_upload_tool_name="teensyloader"))
+ return boards
+
+
+def main():
+ """This detects and then displays all attached discovery boards."""
+
+ pw_arduino_build.log.install(logging.INFO)
+
+ boards = detect_boards()
+ if not boards:
+ _LOG.info("No attached boards detected")
+ for idx, board in enumerate(boards):
+ _LOG.info("Board %d:", idx)
+ _LOG.info(" - Name: %s", board.label)
+ _LOG.info(" - Port: %s", board.dev_name)
+ _LOG.info(" - Address: %s", board.usb_device_path)
+ _LOG.info(" - Test runner args: %s",
+ " ".join(board.test_runner_args()))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pw_arduino_build/py/pw_arduino_build/unit_test_client.py b/pw_arduino_build/py/pw_arduino_build/unit_test_client.py
new file mode 100755
index 000000000..38227f56a
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/unit_test_client.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Launch a pw_target_runner client that sends a test request."""
+
+import argparse
+import subprocess
+import sys
+from typing import Optional
+
+_TARGET_CLIENT_COMMAND = 'pw_target_runner_client'
+
+
+def parse_args():
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('binary', help='The target test binary to run')
+ parser.add_argument('--server-port',
+ type=int,
+ default=8081,
+ help='Port the test server is located on')
+ parser.add_argument('runner_args',
+ nargs=argparse.REMAINDER,
+ help='Arguments to forward to the test runner')
+
+ return parser.parse_args()
+
+
+def launch_client(binary: str, server_port: Optional[int]) -> int:
+ """Sends a test request to the specified server port."""
+ cmd = [_TARGET_CLIENT_COMMAND, '-binary', binary]
+
+ if server_port is not None:
+ cmd.extend(['-port', str(server_port)])
+
+ return subprocess.call(cmd)
+
+
+def main() -> int:
+ """Launch a test by sending a request to a pw_target_runner_server."""
+ args = parse_args()
+ return launch_client(args.binary, args.server_port)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py b/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py
new file mode 100755
index 000000000..24ae65512
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py
@@ -0,0 +1,340 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""This script flashes and runs unit tests onto Arduino boards."""
+
+import argparse
+import logging
+import os
+import re
+import subprocess
+import sys
+import time
+from typing import List
+
+import serial
+import pw_arduino_build.log
+from pw_arduino_build import teensy_detector
+from pw_arduino_build.file_operations import decode_file_json
+
+_LOG = logging.getLogger('unit_test_runner')
+
+# Verification of test pass/failure depends on these strings. If the formatting
+# or output of the simple_printing_event_handler changes, this may need to be
+# updated.
+_TESTS_STARTING_STRING = b'[==========] Running all tests.'
+_TESTS_DONE_STRING = b'[==========] Done running all tests.'
+_TEST_FAILURE_STRING = b'[ FAILED ]'
+
+# How long to wait for the first byte of a test to be emitted. This is longer
+# than the user-configurable timeout as there's a delay while the device is
+# flashed.
+_FLASH_TIMEOUT = 5.0
+
+
+class TestingFailure(Exception):
+ """A simple exception to be raised when a testing step fails."""
+
+
+class DeviceNotFound(Exception):
+ """A simple exception to be raised when unable to connect to a device."""
+
+
+class ArduinoCoreNotSupported(Exception):
+ """Exception raised when a given core does not support unit testing."""
+
+
+def parse_args():
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('binary', help='The target test binary to run')
+ parser.add_argument('--port',
+ help='The name of the serial port to connect to when '
+ 'running tests')
+ parser.add_argument('--baud',
+ type=int,
+ default=115200,
+ help='Target baud rate to use for serial communication'
+ ' with target device')
+ parser.add_argument('--test-timeout',
+ type=float,
+ default=5.0,
+ help='Maximum communication delay in seconds before a '
+ 'test is considered unresponsive and aborted')
+ parser.add_argument('--verbose',
+ '-v',
+ dest='verbose',
+ action="store_true",
+ help='Output additional logs as the script runs')
+
+ # arduino_builder arguments
+ # TODO(tonymd): Get these args from __main__.py or elsewhere.
+ parser.add_argument("-c",
+ "--config-file",
+ required=True,
+ help="Path to a config file.")
+ parser.add_argument("--arduino-package-path",
+ help="Path to the arduino IDE install location.")
+ parser.add_argument("--arduino-package-name",
+ help="Name of the Arduino board package to use.")
+ parser.add_argument("--compiler-path-override",
+ help="Path to arm-none-eabi-gcc bin folder. "
+ "Default: Arduino core specified gcc")
+ parser.add_argument("--board", help="Name of the Arduino board to use.")
+ parser.add_argument("--upload-tool",
+ required=True,
+ help="Name of the Arduino upload tool to use.")
+ parser.add_argument("--set-variable",
+ action="append",
+ metavar='some.variable=NEW_VALUE',
+ help="Override an Arduino recipe variable. May be "
+ "specified multiple times. For example: "
+ "--set-variable 'serial.port.label=/dev/ttyACM0' "
+ "--set-variable 'serial.port.protocol=Teensy'")
+ return parser.parse_args()
+
+
+def log_subprocess_output(level, output):
+ """Logs subprocess output line-by-line."""
+
+ lines = output.decode('utf-8', errors='replace').splitlines()
+ for line in lines:
+ _LOG.log(level, line)
+
+
+def read_serial(port, baud_rate, test_timeout) -> bytes:
+ """Reads lines from a serial port until a line read times out.
+
+ Returns bytes object containing the read serial data.
+ """
+
+ serial_data = bytearray()
+ device = serial.Serial(baudrate=baud_rate,
+ port=port,
+ timeout=_FLASH_TIMEOUT)
+ if not device.is_open:
+ raise TestingFailure('Failed to open device')
+
+ # Flush input buffer and reset the device to begin the test.
+ device.reset_input_buffer()
+
+ # Block and wait for the first byte.
+ serial_data += device.read()
+ if not serial_data:
+ raise TestingFailure('Device not producing output')
+
+ device.timeout = test_timeout
+
+ # Read with a reasonable timeout until we stop getting characters.
+ while True:
+ bytes_read = device.readline()
+ if not bytes_read:
+ break
+ serial_data += bytes_read
+ if serial_data.rfind(_TESTS_DONE_STRING) != -1:
+ # Set to much more aggressive timeout since the last one or two
+ # lines should print out immediately. (one line if all fails or all
+ # passes, two lines if mixed.)
+ device.timeout = 0.01
+
+ # Remove carriage returns.
+ serial_data = serial_data.replace(b'\r', b'')
+
+ # Try to trim captured results to only contain most recent test run.
+ test_start_index = serial_data.rfind(_TESTS_STARTING_STRING)
+ return serial_data if test_start_index == -1 else serial_data[
+ test_start_index:]
+
+
+def wait_for_port(port):
+ """Wait for the path to the port to be available."""
+ # Wait for the port to exist
+ while not os.path.exists(port):
+ time.sleep(1)
+ # Wait for the port to be accessible
+ while not os.access(port, os.R_OK):
+ time.sleep(1)
+
+
+def flash_device(test_runner_args, upload_tool):
+ """Flash binary to a connected device using the provided configuration."""
+
+ # TODO(tonymd): Create a library function to call rather than launching
+ # the arduino_builder script.
+ flash_tool = 'arduino_builder'
+ cmd = [flash_tool, "--quiet"] + test_runner_args + [
+ "--run-objcopy", "--run-postbuilds", "--run-upload", upload_tool
+ ]
+ _LOG.info('Flashing firmware to device')
+ _LOG.debug('Running: %s', " ".join(cmd))
+
+ env = os.environ.copy()
+ process = subprocess.run(cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ env=env)
+ if process.returncode:
+ log_subprocess_output(logging.ERROR, process.stdout)
+ raise TestingFailure('Failed to flash target device')
+
+ log_subprocess_output(logging.DEBUG, process.stdout)
+
+ _LOG.debug('Successfully flashed firmware to device')
+
+
+def handle_test_results(test_output):
+ """Parses test output to determine whether tests passed or failed."""
+
+ if test_output.find(_TESTS_STARTING_STRING) == -1:
+ raise TestingFailure('Failed to find test start')
+
+ if test_output.rfind(_TESTS_DONE_STRING) == -1:
+ log_subprocess_output(logging.INFO, test_output)
+ raise TestingFailure('Tests did not complete')
+
+ if test_output.rfind(_TEST_FAILURE_STRING) != -1:
+ log_subprocess_output(logging.INFO, test_output)
+ raise TestingFailure('Test suite had one or more failures')
+
+ log_subprocess_output(logging.DEBUG, test_output)
+
+ _LOG.info('Test passed!')
+
+
+def run_device_test(binary, port, baud, test_timeout, upload_tool,
+ arduino_package_path, test_runner_args) -> bool:
+ """Flashes, runs, and checks an on-device test binary.
+
+ Returns true on test pass.
+ """
+ if test_runner_args is None:
+ test_runner_args = []
+
+ if "teensy" not in arduino_package_path:
+ raise ArduinoCoreNotSupported(arduino_package_path)
+
+ if port is None or "--set-variable" not in test_runner_args:
+ _LOG.debug('Attempting to automatically detect dev board')
+ boards = teensy_detector.detect_boards(arduino_package_path)
+ if not boards:
+ error = 'Could not find an attached device'
+ _LOG.error(error)
+ raise DeviceNotFound(error)
+ test_runner_args += boards[0].test_runner_args()
+ upload_tool = boards[0].arduino_upload_tool_name
+ if port is None:
+ port = boards[0].dev_name
+
+ _LOG.debug('Launching test binary %s', binary)
+ try:
+ result: List[bytes] = []
+ _LOG.info('Running test')
+ # Warning: A race condition is possible here. This assumes the host is
+ # able to connect to the port and that there isn't a test running on
+ # this serial port.
+ flash_device(test_runner_args, upload_tool)
+ wait_for_port(port)
+ result.append(read_serial(port, baud, test_timeout))
+ if result:
+ handle_test_results(result[0])
+ except TestingFailure as err:
+ _LOG.error(err)
+ return False
+
+ return True
+
+
+def get_option(key, config_file_values, args, required=False):
+ command_line_option = getattr(args, key, None)
+ final_option = config_file_values.get(key, command_line_option)
+ if required and command_line_option is None and final_option is None:
+ # Print a similar error message to argparse
+ executable = os.path.basename(sys.argv[0])
+ option = "--" + key.replace("_", "-")
+ print(f"{executable}: error: the following arguments are required: "
+ f"{option}")
+ sys.exit(1)
+ return final_option
+
+
+def main():
+ """Set up runner, and then flash/run device test."""
+ args = parse_args()
+
+ json_file_options, unused_config_path = decode_file_json(args.config_file)
+
+ log_level = logging.DEBUG if args.verbose else logging.INFO
+ pw_arduino_build.log.install(log_level)
+
+ # Construct arduino_builder flash arguments for a given .elf binary.
+ arduino_package_path = get_option("arduino_package_path",
+ json_file_options,
+ args,
+ required=True)
+ # Arduino core args.
+ arduino_builder_args = [
+ "--arduino-package-path",
+ arduino_package_path,
+ "--arduino-package-name",
+ get_option("arduino_package_name",
+ json_file_options,
+ args,
+ required=True),
+ ]
+
+ # Use CIPD installed compilers.
+ compiler_path_override = get_option("compiler_path_override",
+ json_file_options, args)
+ if compiler_path_override:
+ arduino_builder_args += [
+ "--compiler-path-override", compiler_path_override
+ ]
+
+ # Run subcommand with board selection arg.
+ arduino_builder_args += [
+ "run", "--board",
+ get_option("board", json_file_options, args, required=True)
+ ]
+
+ # .elf file location args.
+ build_path = os.path.dirname(args.binary)
+ arduino_builder_args += ["--build-path", build_path]
+ build_project_name = os.path.basename(args.binary)
+ # Remove '.elf' extension.
+ match_result = re.match(r'(.*?)\.elf$', os.path.basename(args.binary),
+ re.IGNORECASE)
+ if match_result:
+ build_project_name = match_result[1]
+ arduino_builder_args += ["--build-project-name", build_project_name]
+
+ # USB port is passed to arduino_builder_args via --set-variable args.
+ if args.set_variable:
+ for var in args.set_variable:
+ arduino_builder_args += ["--set-variable", var]
+
+ if run_device_test(args.binary,
+ args.port,
+ args.baud,
+ args.test_timeout,
+ args.upload_tool,
+ arduino_package_path,
+ test_runner_args=arduino_builder_args):
+ sys.exit(0)
+ else:
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_arduino_build/py/pw_arduino_build/unit_test_server.py b/pw_arduino_build/py/pw_arduino_build/unit_test_server.py
new file mode 100644
index 000000000..0fafd71fb
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/unit_test_server.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Launch a pw_test_server server to use for multi-device testing."""
+
+import argparse
+import logging
+import sys
+import tempfile
+from typing import IO, List, Optional
+
+import pw_cli.process
+import pw_arduino_build.log
+from pw_arduino_build import teensy_detector
+from pw_arduino_build.file_operations import decode_file_json
+from pw_arduino_build.unit_test_runner import ArduinoCoreNotSupported
+
+_LOG = logging.getLogger('unit_test_server')
+
+_TEST_RUNNER_COMMAND = 'arduino_unit_test_runner'
+
+_TEST_SERVER_COMMAND = 'pw_target_runner_server'
+
+
+class UnknownArduinoCore(Exception):
+ """Exception raised when no Arduino core can be found."""
+
+
+def parse_args():
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--server-port',
+ type=int,
+ default=8081,
+ help='Port to launch the pw_target_runner_server on')
+ parser.add_argument('--server-config',
+ type=argparse.FileType('r'),
+ help='Path to server config file')
+ parser.add_argument('--verbose',
+ '-v',
+ dest='verbose',
+ action="store_true",
+ help='Output additional logs as the script runs')
+ parser.add_argument("-c",
+ "--config-file",
+ required=True,
+ help="Path to an arduino_builder config file.")
+ # TODO(tonymd): Explicitly split args using "--". See example in:
+ # //pw_unit_test/py/pw_unit_test/test_runner.py:326
+ parser.add_argument('runner_args',
+ nargs=argparse.REMAINDER,
+ help='Arguments to forward to the test runner')
+
+ return parser.parse_args()
+
+
+def generate_runner(command: str, arguments: List[str]) -> str:
+ """Generates a text-proto style pw_target_runner_server configuration."""
+ # TODO(amontanez): Use a real proto library to generate this when we have
+ # one set up.
+ for i, arg in enumerate(arguments):
+ arguments[i] = f' args: "{arg}"'
+ runner = ['runner {', f' command:"{command}"']
+ runner.extend(arguments)
+ runner.append('}\n')
+ return '\n'.join(runner)
+
+
+def generate_server_config(runner_args: Optional[List[str]],
+ arduino_package_path: str) -> IO[bytes]:
+ """Returns a temporary generated file for use as the server config."""
+
+ if "teensy" not in arduino_package_path:
+ raise ArduinoCoreNotSupported(arduino_package_path)
+
+ boards = teensy_detector.detect_boards(arduino_package_path)
+ if not boards:
+ _LOG.critical('No attached boards detected')
+ sys.exit(1)
+ config_file = tempfile.NamedTemporaryFile()
+ _LOG.debug('Generating test server config at %s', config_file.name)
+ _LOG.debug('Found %d attached devices', len(boards))
+ for board in boards:
+ test_runner_args = []
+ if runner_args:
+ test_runner_args += runner_args
+ test_runner_args += ["-v"] + board.test_runner_args()
+ test_runner_args += ["--port", board.dev_name]
+ test_runner_args += ["--upload-tool", board.arduino_upload_tool_name]
+ config_file.write(
+ generate_runner(_TEST_RUNNER_COMMAND,
+ test_runner_args).encode('utf-8'))
+ config_file.flush()
+ return config_file
+
+
+def launch_server(server_config: Optional[IO[bytes]],
+ server_port: Optional[int], runner_args: Optional[List[str]],
+ arduino_package_path: str) -> int:
+ """Launch a device test server with the provided arguments."""
+ if server_config is None:
+ # Auto-detect attached boards if no config is provided.
+ server_config = generate_server_config(runner_args,
+ arduino_package_path)
+
+ cmd = [_TEST_SERVER_COMMAND, '-config', server_config.name]
+
+ if server_port is not None:
+ cmd.extend(['-port', str(server_port)])
+
+ return pw_cli.process.run(*cmd, log_output=True).returncode
+
+
+def main():
+ """Launch a device test server with the provided arguments."""
+ args = parse_args()
+
+ if "--" in args.runner_args:
+ args.runner_args.remove("--")
+
+ log_level = logging.DEBUG if args.verbose else logging.INFO
+ pw_arduino_build.log.install(log_level)
+
+ # Get arduino_package_path from either the config file or command line args.
+ arduino_package_path = None
+ if args.config_file:
+ json_file_options, unused_config_path = decode_file_json(
+ args.config_file)
+ arduino_package_path = json_file_options.get("arduino_package_path",
+ None)
+ # Must pass --config-file option in the runner_args.
+ if "--config-file" not in args.runner_args:
+ args.runner_args.append("--config-file")
+ args.runner_args.append(args.config_file)
+
+ # Check for arduino_package_path in the runner_args
+ try:
+ arduino_package_path = args.runner_args[
+ args.runner_args.index("--arduino-package-path") + 1]
+ except (ValueError, IndexError):
+ # Only raise an error if arduino_package_path not set from the json.
+ if arduino_package_path is None:
+ raise UnknownArduinoCore("Test runner arguments: '{}'".format(
+ " ".join(args.runner_args)))
+
+ exit_code = launch_server(args.server_config, args.server_port,
+ args.runner_args, arduino_package_path)
+ sys.exit(exit_code)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_arduino_build/py/setup.py b/pw_arduino_build/py/setup.py
index 1cb074f75..0c7afc60b 100644
--- a/pw_arduino_build/py/setup.py
+++ b/pw_arduino_build/py/setup.py
@@ -23,8 +23,16 @@ setuptools.setup(
description='Target-specific python scripts for the arduino target',
packages=setuptools.find_packages(),
entry_points={
- 'console_scripts':
- ['arduino_builder = pw_arduino_build.__main__:main']
+ 'console_scripts': [
+ 'arduino_builder = pw_arduino_build.__main__:main',
+ 'teensy_detector = pw_arduino_build.teensy_detector:main',
+ 'arduino_unit_test_runner = '
+ ' pw_arduino_build.unit_test_runner:main',
+ 'arduino_test_server = '
+ ' pw_arduino_build.unit_test_server:main',
+ 'arduino_test_client = '
+ ' pw_arduino_build.unit_test_client:main',
+ ]
},
install_requires=[
'pyserial',