mbed-os/hal/tests/pinvalidate/pinvalidate.py

932 lines
27 KiB
Python
Executable File

#!/usr/bin/env python
"""
Copyright (c) 2020 ARM Limited
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
http://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.
"""
import argparse
import json
import pathlib
import hashlib
import re
import sys
from tabulate import tabulate
from itertools import chain
from enum import Enum
class ReturnCode(Enum):
"""Return codes."""
SUCCESS = 0
ERROR = 1
INVALID_OPTIONS = 2
class TestCaseError(Exception):
"""An exception for test case failure."""
class ArgumentParserWithDefaultHelp(argparse.ArgumentParser):
"""Subclass that always shows the help message on invalid arguments."""
def error(self, message):
"""Error handler."""
sys.stderr.write("error: {}\n".format(message))
self.print_help()
raise SystemExit(ReturnCode.INVALID_OPTIONS.value)
def find_target_by_path(target_path):
"""Find a target by path."""
mbed_os_root = pathlib.Path(__file__).absolute().parents[3]
targets = dict()
with open(target_path) as pin_names_file:
pin_names_file_content = pin_names_file.read()
target_list_match = re.search(
"\/* MBED TARGET LIST: ([0-9A-Z_,* \n]+)*\/",
pin_names_file_content
)
target_list = []
if target_list_match:
target_list = list(
re.findall(
r"([0-9A-Z_]{3,})",
target_list_match.group(1),
re.MULTILINE,
)
)
if not target_list:
print("WARNING: MBED TARGET LIST marker invalid or not found in file " + target_path)
print("Target could not be determined. Only the generic test suite will run. You can manually specify additional suites.")
with (
mbed_os_root.joinpath("targets", "targets.json")
).open() as targets_json_file:
target_data = json.load(targets_json_file)
# find target in targets.json
for target in target_data:
if "public" in target_data[target]:
if not target_data[target]["public"]:
continue
if target in target_list:
targets[target] = target_path
if len(targets) == 0:
targets[target_path] = target_path
return targets
def find_target_by_name(target_name=""):
"""Find a target by name."""
mbed_os_root = pathlib.Path(__file__).absolute().parents[3]
targets = dict()
for f in mbed_os_root.joinpath('targets').rglob("PinNames.h"):
with open(f) as pin_names_file:
pin_names_file_content = pin_names_file.read()
target_list_match = re.search(
"\/* MBED TARGET LIST: ([0-9A-Z_,* \n]+)*\/",
pin_names_file_content
)
target_list = []
if target_list_match:
target_list = list(
re.findall(
r"([0-9A-Z_]{3,})",
target_list_match.group(1),
re.MULTILINE,
)
)
if target_name:
if target_name in target_list:
targets[target_name] = f
break
else:
for target in target_list:
targets[target] = f
return targets
def check_markers(test_mode=False):
"""Validate markers in PinNames.h files"""
mbed_os_root = pathlib.Path(__file__).absolute().parents[3]
errors = []
with (
mbed_os_root.joinpath("targets", "targets.json")
).open() as targets_json_file:
targets_json = json.load(targets_json_file)
if test_mode:
search_dir = pathlib.Path(__file__).parent.joinpath('test_files').absolute()
else:
search_dir = mbed_os_root.joinpath('targets')
for f in search_dir.rglob("PinNames.h"):
with open(f) as pin_names_file:
pin_names_file_content = pin_names_file.read()
target_list_match = re.search(
"\/* MBED TARGET LIST: ([0-9A-Z_,* \n]+)*\/",
pin_names_file_content
)
marker_target_list = []
if target_list_match:
marker_target_list = list(
re.findall(
r"([0-9A-Z_]{3,})",
target_list_match.group(1),
re.MULTILINE,
)
)
if not marker_target_list:
print("WARNING: MBED TARGET LIST marker invalid or not found in file " + str(f))
errors.append({ "file": str(f), "error": "marker invalid or not found"})
continue
for target in marker_target_list:
target_is_valid = False
if target in targets_json:
target_is_valid = True
if "public" in targets_json[target]:
if targets_json[target]["public"] == False:
target_is_valid = False
if not target_is_valid:
print("WARNING: MBED TARGET LIST in file " + str(f) + " includes target '" + target + "' which doesn't exist in targets.json or is not public")
errors.append({ "file": str(f), "error": "target not found"})
return errors
def check_duplicate_pinnames_files(test_mode=False):
"""Check for duplicate PinNames.h files"""
mbed_os_root = pathlib.Path(__file__).absolute().parents[3]
errors = []
file_hash_dict = dict()
if test_mode:
search_dir = pathlib.Path(__file__).parent.joinpath('test_files').absolute()
else:
search_dir = mbed_os_root.joinpath('targets')
for f in search_dir.rglob("PinNames.h"):
with open(f) as pin_names_file:
pin_names_file_content = pin_names_file.read()
file_hash_dict[str(f)] = hashlib.md5(pin_names_file_content.encode('utf-8')).hexdigest()
rev_dict = {}
for key, value in file_hash_dict.items():
rev_dict.setdefault(value, set()).add(key)
duplicates = [key for key, values in rev_dict.items()
if len(values) > 1]
for duplicate in duplicates:
print("WARNING: Duplicate files")
for file_path, file_hash in file_hash_dict.items():
if file_hash == duplicate:
errors.append({ "file": file_path, "error": "duplicate file"})
print("\t" + file_path)
return errors
def check_duplicate_markers(test_mode=False):
"""Check target markers in PinNames.h files for duplicates."""
mbed_os_root = pathlib.Path(__file__).absolute().parents[3]
errors = []
markers = dict()
if test_mode:
search_dir = pathlib.Path(__file__).parent.joinpath('test_files').absolute()
else:
search_dir = mbed_os_root.joinpath('targets')
for f in search_dir.rglob("PinNames.h"):
with open(f) as pin_names_file:
pin_names_file_content = pin_names_file.read()
target_list_match = re.search(
"\/* MBED TARGET LIST: ([0-9A-Z_,* \n]+)*\/",
pin_names_file_content
)
marker_target_list = []
if target_list_match:
marker_target_list = list(
re.findall(
r"([0-9A-Z_]{3,})",
target_list_match.group(1),
re.MULTILINE,
)
)
for target in marker_target_list:
if target in markers:
print("WARNING: target duplicate in " + str(f) + ", " + target + " first listed in " + markers[target])
errors.append({ "file": str(f), "error": "duplicate marker"})
else:
markers[target] = str(f)
return errors
def target_has_form_factor(target_name, form_factor):
"""Check if the target has the Arduino form factor."""
mbed_os_root = pathlib.Path(__file__).absolute().parents[3]
with (
mbed_os_root.joinpath("targets", "targets.json")
).open() as targets_json_file:
target_data = json.load(targets_json_file)
if target_name in target_data:
if "supported_form_factors" in target_data[target_name]:
form_factors = target_data[target_name]["supported_form_factors"]
if form_factor in form_factors:
return True
return False
def pin_name_to_dict(pin_name_file_content):
pin_name_enum_dict = dict()
pin_name_enum_match = re.search(
"typedef enum {\n([^}]*)\n} PinName;", pin_name_file_content
)
if pin_name_enum_match:
pin_name_enum_body = pin_name_enum_match.group(1)
pin_name_enum_dict = dict(
re.findall(
r"^\s*([a-zA-Z0-9_]+)\s*=\s*([a-zA-Z0-9_]+)",
pin_name_enum_body,
re.MULTILINE,
)
)
pin_name_define_dict = dict(
re.findall(
r"^#define\s+([a-zA-Z0-9_]+)\s+([a-zA-Z0-9_]+)",
pin_name_file_content,
re.MULTILINE,
)
)
return {**pin_name_enum_dict, **pin_name_define_dict}
def identity_assignment_check(pin_name_dict):
invalid_items = []
for key, val in pin_name_dict.items():
if val == key:
message = "cannot assign value to itself"
invalid_items.append({"key": key, "val": val, "message": message})
return invalid_items
def nc_assignment_check(pin_name_dict):
invalid_items = []
for key, val in pin_name_dict.items():
if re.match(r"^((LED|BUTTON)\d*|CONSOLE_TX|CONSOLE_RX|USBTX|USBRX)$", key):
if val == "NC":
message = "cannot be NC"
invalid_items.append(
{"key": key, "val": val, "message": message}
)
return invalid_items
def duplicate_assignment_check(pin_name_dict):
used_pins = []
used_pins_friendly = []
invalid_items = []
for key, val in pin_name_dict.items():
if re.match(r"^((LED|BUTTON)\d*|CONSOLE_TX|CONSOLE_RX|USBTX|USBRX)$", key):
if val == "NC":
continue
# resolve to literal
realval = val
depth = 0
while not re.match(
"(0x[0-9a-fA-F]+|[1-9][0-9]*|0[1-7][0-7]+|0b[01]+)[uUlL]{0,2}",
realval,
):
try:
realval = pin_name_dict[realval]
depth += 1
except KeyError:
break
if depth > 10:
break
if realval in used_pins:
message = (
"already assigned to "
+ used_pins_friendly[used_pins.index(realval)]
)
invalid_items.append(
{"key": key, "val": val, "message": message}
)
continue
used_pins.append(realval)
used_pins_friendly.append(key + " = " + val)
return invalid_items
def arduino_duplicate_assignment_check(pin_name_dict):
used_pins = []
used_pins_friendly = []
invalid_items = []
for key, val in pin_name_dict.items():
if re.match(r"^ARDUINO_UNO_[AD]\d+$", key):
if val == "NC":
continue
# resolve to literal
realval = val
depth = 0
while not re.match(
"(0x[0-9a-fA-F]+|[1-9][0-9]*|0[1-7][0-7]+|0b[01]+)[uUlL]{0,2}",
realval,
):
try:
realval = pin_name_dict[realval]
depth += 1
except KeyError:
break
if depth > 10:
break
if realval in used_pins:
message = (
"already assigned to "
+ used_pins_friendly[used_pins.index(realval)]
)
invalid_items.append(
{"key": key, "val": val, "message": message}
)
continue
used_pins.append(realval)
used_pins_friendly.append(key + " = " + val)
return invalid_items
def arduino_existence_check(pin_name_dict):
analog_pins = (f"ARDUINO_UNO_A{i}" for i in range(6))
digital_pins = (f"ARDUINO_UNO_D{i}" for i in range(16))
return [
{"key": pin, "val": "", "message": pin + " not defined"}
for pin in chain(analog_pins, digital_pins)
if pin not in pin_name_dict
]
def arduino_nc_assignment_check(pin_name_dict):
invalid_items = []
for key, val in pin_name_dict.items():
if re.match(r"^ARDUINO_UNO_[AD]\d+$", key):
if val == "NC":
message = "cannot be NC"
invalid_items.append(
{"key": key, "val": val, "message": message}
)
return invalid_items
def legacy_assignment_check(pin_name_content):
invalid_items = []
legacy_assignments = dict(
re.findall(
r"^\s*((?:LED|BUTTON)\d*)\s*=\s*([a-zA-Z0-9_]+)",
pin_name_content,
re.MULTILINE,
)
)
for key, val in legacy_assignments.items():
message = "legacy assignment; LEDs and BUTTONs must be #define'd"
invalid_items.append({"key": key, "val": val, "message": message})
return invalid_items
def legacy_alias_check(pin_name_content):
invalid_items = []
legacy_assignments = dict(
re.findall(
r"^\s*((?:SPI|I2C)_\w*)\s*=\s*([a-zA-Z0-9_]+)",
pin_name_content,
re.MULTILINE,
)
)
for key, val in legacy_assignments.items():
message = "legacy assignment; SPI_xxx and I2C_xxx must be #define'd"
invalid_items.append({"key": key, "val": val, "message": message})
return invalid_items
def legacy_uart_check(pin_name_dict):
invalid_items = []
if "CONSOLE_TX" not in pin_name_dict or "CONSOLE_RX" not in pin_name_dict:
message = "CONSOLE_TX or CONSOLE_RX are not defined; USBTX and USBRX are deprecated"
invalid_items.append({"key": "", "val": "", "message": message})
return invalid_items
def legacy_arduino_uno_check(arduino_form_factor):
invalid_items = []
if arduino_form_factor == True:
message = "ARDUINO form factor is deprecated, should be replaced by ARDUINO_UNO"
invalid_items.append({"key": "", "val": "", "message": message})
return invalid_items
def print_summary(report):
targets = set([case["platform_name"] for case in report])
table = []
for target in targets:
error_count = 0
for case in report:
if (
case["platform_name"] == target
and case["result"] == "FAILED"
):
error_count += 1
table.append(
[target, "FAILED" if error_count else "PASSED", error_count]
)
return tabulate(
table,
headers=["Platform name", "Result", "Error count"],
tablefmt="grid",
)
def print_suite_summary(report):
targets = set([case["platform_name"] for case in report])
table = []
for target in targets:
suites = set([
case["suite_name"]
for case in report
if case["platform_name"] == target
])
for suite in suites:
result = "PASSED"
error_count = 0
for case in report:
if case["platform_name"] != target:
continue
if case["suite_name"] != suite:
continue
if case["result"] == "FAILED":
result = "FAILED"
error_count += 1
table.append([target, suite, result, error_count])
return tabulate(
table,
headers=["Platform name", "Test suite", "Result", "Error count"],
tablefmt="grid",
)
def print_report(report, print_error_detail, tablefmt="grid"):
table = []
for case in report:
errors_str = []
for error in case["errors"]:
errors_str.append("\n")
if error["key"]:
errors_str.append(error["key"])
if error["val"]:
errors_str.append(" = ")
errors_str.append(error["val"])
if error["message"]:
errors_str.append(" <-- ")
errors_str.append(error["message"])
if not errors_str:
errors_str = "None"
if print_error_detail:
table.append(
(
case["platform_name"],
case["suite_name"],
case["case_name"],
case["result"],
len(case["errors"]),
"None" if not errors_str else "".join(errors_str).lstrip(),
)
)
else:
table.append(
(
case["platform_name"],
case["suite_name"],
case["case_name"],
case["result"],
len(case["errors"]),
)
)
return tabulate(
table,
headers=[
"Platform name",
"Test suite",
"Test case",
"Result",
"Error count",
"Errors",
],
tablefmt=tablefmt,
)
def print_pretty_html_report(report):
output = []
output.append("<html><head><style>table, td, tr { border: 2px solid black; border-collapse: collapse; padding: 5px; font-family: Helvetica, serif; }</style></head>")
output.append('<body><p><button onclick=\'e=document.getElementsByTagName("details");for(var i=0;i<e.length;i++){e[i].setAttribute("open","true")};\'>Expand all errors</button></p><table>')
output.append("<tr><th>Platform name</th><th>Test suite</th><th>Test case</th><th>Result</th><th>Error count</th><th>Errors</th></tr>")
for case in report:
output.append("<tr>")
if case["errors"]:
error_details = ["<details><summary>View errors</summary><table>"]
for error in case["errors"]:
error_details.append("<tr>")
if error["key"]:
error_details.append("<td>")
error_details.append(error["key"])
error_details.append("</td>")
if error["val"]:
error_details.append("<td>")
error_details.append(error["val"])
error_details.append("</td>")
if error["message"]:
error_details.append("<td>")
error_details.append(error["message"])
error_details.append("</td>")
error_details.append("</tr>")
error_details.append("</table></details>")
else:
error_details = []
output.append("<td>")
output.append(case["platform_name"])
output.append("</td>")
output.append("<td>")
output.append(case["suite_name"])
output.append("</td>")
output.append("<td>")
output.append(case["case_name"])
output.append("</td>")
if case["result"] == "PASSED":
color = "green"
count_color = "black"
else:
color = "red"
count_color = "red"
output.append("<td style='color:")
output.append(color)
output.append("'>")
output.append(case["result"])
output.append("</td>")
output.append("<td style='color:")
output.append(count_color)
output.append("'>")
output.append(str(len(case["errors"])))
output.append("</td>")
output.append("<td>")
output.extend(error_details)
output.append("</td>")
output.append("</tr>")
output.append("</table></body></table>")
return "".join(output)
def has_passed_all_test_cases(report):
"""Check that all test cases passed."""
for case in report:
if case["result"] == "FAILED":
return False
return True
test_cases = [
{
"suite_name": "generic",
"case_name": "identity",
"case_function": identity_assignment_check,
"case_input": "dict",
},
{
"suite_name": "generic",
"case_name": "nc",
"case_function": nc_assignment_check,
"case_input": "dict",
},
{
"suite_name": "generic",
"case_name": "duplicate",
"case_function": duplicate_assignment_check,
"case_input": "dict",
},
{
"suite_name": "generic",
"case_name": "legacy",
"case_function": legacy_assignment_check,
"case_input": "content",
},
{
"suite_name": "generic",
"case_name": "alias",
"case_function": legacy_alias_check,
"case_input": "content",
},
{
"suite_name": "generic",
"case_name": "uart",
"case_function": legacy_uart_check,
"case_input": "content",
},
{
"suite_name": "generic",
"case_name": "arduino_formfactor",
"case_function": legacy_arduino_uno_check,
"case_input": "arduino_form_factor",
},
{
"suite_name": "arduino_uno",
"case_name": "duplicate",
"case_function": arduino_duplicate_assignment_check,
"case_input": "dict",
},
{
"suite_name": "arduino_uno",
"case_name": "existence",
"case_function": arduino_existence_check,
"case_input": "dict",
},
{
"suite_name": "arduino_uno",
"case_name": "nc",
"case_function": arduino_nc_assignment_check,
"case_input": "dict",
},
]
def validate_pin_names(args):
"""Entry point for validating the Pin names."""
suites = []
if args.suite_names:
suites = args.suite_names.split(",")
targets = dict()
if args.paths:
paths = args.paths.split(",")
for path in paths:
targets = {**targets, **find_target_by_path(path)}
elif args.targets:
target_names = args.targets.split(",")
for target_name in target_names:
targets = {**targets, **find_target_by_name(target_name)}
elif args.all:
targets = find_target_by_name()
elif args.check_markers:
check_markers()
check_duplicate_pinnames_files()
check_duplicate_markers()
return
report = []
for target, path in targets.items():
pin_name_content = open(path).read()
pin_name_dict = pin_name_to_dict(pin_name_content)
arduino_uno_support = target_has_form_factor(target, "ARDUINO_UNO")
arduino_support = target_has_form_factor(target, "ARDUINO")
for case in test_cases:
if suites:
if case["suite_name"] not in suites:
continue
else:
if not arduino_uno_support and case["suite_name"] == "arduino_uno":
continue
if not arduino_uno_support and not arduino_support and case["case_name"] == "arduino_formfactor":
continue
if case["case_input"] == "dict":
case_input = pin_name_dict
elif case["case_input"] == "content":
case_input = pin_name_content
elif case["case_input"] == "arduino_form_factor":
case_input = arduino_support
case_output = case["case_function"](case_input)
case_result = "FAILED" if case_output else "PASSED"
platform_name = target
if not args.full_name and args.output_format == "prettytext":
if len(platform_name) > 40:
platform_name = "..." + platform_name[-40:]
report.append(
{
"platform_name": platform_name,
"suite_name": case["suite_name"],
"case_name": case["case_name"],
"result": case_result,
"errors": case_output,
}
)
generate_output(report, args.output_format, args.verbose, args.output_file)
if not has_passed_all_test_cases(report):
raise TestCaseError("One or more test cases failed")
def generate_output(report, output_format, verbosity, output_file):
"""Generate the output."""
if output_format == "json":
output = json.dumps(report)
elif output_format == "html":
output = print_pretty_html_report(report)
else:
if verbosity == 0:
output = print_summary(report)
elif verbosity == 1:
output = print_suite_summary(report)
elif verbosity == 2:
output = print_report(report, False)
elif verbosity > 2:
output = print_report(report, True)
if output_file:
with open(output_file, "w") as out_file:
out_file.write(output)
else:
print(output)
def parse_args():
"""Parse the command line args."""
parser = ArgumentParserWithDefaultHelp(
description="Pin names validation",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-n",
"--suite_names",
help="Run specific test suite. Use comma to seperate multiple suites.",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help=(
"Verbosity of the report (none to -vvv)."
" Only applies for 'prettytext' output format."
),
)
parser.add_argument(
"-f",
"--full_name",
action="store_true",
help=(
"Don't truncate long platform names in"
" human-readable output formats"
),
)
parser.add_argument(
"-o",
"--output_format",
default="prettytext",
help="Set the output format: prettytext (default), json or html",
)
parser.add_argument(
"-w",
"--output_file",
help="File to write output to, instead of printing to stdout",
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"-t",
"--targets",
help=(
"Target name. Use comma to seperate multiple targets."
"THIS FEATURE IS EXPERIMENTAL!"
),
)
group.add_argument(
"-p",
"--paths",
help="Path to PinNames.h file. Use comma to seperate multiple paths.",
)
group.add_argument(
"-a", "--all", action="store_true", help="Run tests on all targets."
)
group.add_argument(
"-m",
"--check-markers",
action="store_true",
help="Check all PinNames.h for the MBED TARGET LIST marker."
)
parser.set_defaults(func=validate_pin_names)
args_namespace = parser.parse_args()
# We want to fail gracefully, with a consistent
# help message, in the no argument case.
# So here's an obligatory hasattr hack.
if not hasattr(args_namespace, "func"):
parser.error("No arguments given!")
else:
return args_namespace
def run_pin_validate():
"""Application main algorithm."""
args = parse_args()
args.func(args)
def _main():
"""Run pinvalidate."""
try:
run_pin_validate()
except Exception as error:
print(error)
return ReturnCode.ERROR.value
else:
return ReturnCode.SUCCESS.value
if __name__ == "__main__":
sys.exit(_main())