mirror of https://github.com/ARMmbed/mbed-os.git
531 lines
19 KiB
Python
531 lines
19 KiB
Python
# Script to check a new mbed 2 release by compiling a set of specified test apps
|
|
# for all currently supported platforms. Each test app must include an mbed library.
|
|
# This can either be the pre-compiled version 'mbed' or the source version 'mbed-dev'.
|
|
#
|
|
# Setup:
|
|
# 1. Set up your global .hgrc file
|
|
#
|
|
# If you don't already have a .hgrc file in your $HOME directory, create one there.
|
|
# Then add the following section:
|
|
#
|
|
# [auth]
|
|
# x.prefix = *
|
|
# x.username = <put your mbed org username here>
|
|
# x.password = <put your mbed org password here>
|
|
#
|
|
# This has 2 purposes, the first means you don't get prompted for your password
|
|
# whenever you run hg commands on the commandline. The second is that this script
|
|
# reads these details in order to fully automate the Mercurial commands.
|
|
#
|
|
# Edit "check_release.json". This has the following structure:
|
|
#{
|
|
# "config" : {
|
|
# "mbed_repo_path" : "C:/Users/annbri01/Work/Mercurial"
|
|
# },
|
|
# "test_list" : [
|
|
# {
|
|
# "name" : "test_compile_mbed_lib",
|
|
# "lib" : "mbed"
|
|
# },
|
|
# {
|
|
# "name" : "test_compile_mbed_dev",
|
|
# "lib" : "mbed-dev"
|
|
# }
|
|
# ],
|
|
# "target_list" : []
|
|
#}
|
|
#
|
|
# The mbed_repo_path field should be changed to point to where your local
|
|
# working directory is for Mercurial repositories.
|
|
# For each test app you wish to run, add an entry to the test list. The example
|
|
# above has 2 test apps
|
|
# "test_compile_mbed_lib" and "test_compile_mbed_dev"
|
|
# The lib field in each says which type of mbed 2 library the app contains.
|
|
# These test apps MUST be available as repos in the user's online Mercurial area.
|
|
# The target_list allows the user to override the set of targets/platforms used
|
|
# for the compilation.
|
|
# E.g to just compile for 2 targets, K64F and K22F :
|
|
# "target_list" : ["K64F", "K22F"]
|
|
#
|
|
# Run the script from the mbed-os directory as follows:
|
|
# > python tools/check_release.py
|
|
#
|
|
# It will look for local clones of the test app repos. If they don't exist
|
|
# it will clone them. It will then read the latest versions of mbed and mbed-dev
|
|
# (an assumption is made that both of these are already cloned in your Mercurial area).
|
|
# The lib files within the test apps are then updated to the corresponding version in
|
|
# the associated lib itself. The test apps are then committed and pushed back to the users
|
|
# fork.
|
|
# The test apps will then be compiled for all supported targets and a % result output at
|
|
# the end.
|
|
#
|
|
# Uses the online compiler API at https://mbed.org/handbook/Compile-API
|
|
# Based on the example from https://mbed.org/teams/mbed/code/mbed-API-helper/
|
|
|
|
|
|
import os, getpass, sys, json, time, requests, logging
|
|
from os.path import dirname, abspath, basename, join
|
|
import argparse
|
|
import subprocess
|
|
import re
|
|
import hglib
|
|
import argparse
|
|
|
|
# Be sure that the tools directory is in the search path
|
|
ROOT = abspath(join(dirname(__file__), ".."))
|
|
sys.path.insert(0, ROOT)
|
|
|
|
from tools.build_api import get_mbed_official_release
|
|
|
|
OFFICIAL_MBED_LIBRARY_BUILD = get_mbed_official_release('2')
|
|
|
|
def get_compilation_failure(messages):
|
|
""" Reads the json formatted 'messages' and checks for compilation errors.
|
|
If there is a genuine compilation error then there should be a new
|
|
message containing a severity field = Error and an accompanying message
|
|
with the compile error text. Any other combination is considered an
|
|
internal compile engine failure
|
|
Args:
|
|
messages - json formatted text returned by the online compiler API.
|
|
|
|
Returns:
|
|
Either "Error" or "Internal" to indicate an actual compilation error or an
|
|
internal IDE API fault.
|
|
|
|
"""
|
|
for m in messages:
|
|
# Get message text if it exists
|
|
try:
|
|
message = m['message']
|
|
message = message + "\n"
|
|
except KeyError:
|
|
# Skip this message as it has no 'message' field
|
|
continue
|
|
|
|
# Get type of message text
|
|
try:
|
|
msg_type = m['type']
|
|
except KeyError:
|
|
# Skip this message as it has no 'type' field
|
|
continue
|
|
|
|
if msg_type == 'error' or msg_type == 'tool_error':
|
|
rel_log.error(message)
|
|
return "Error"
|
|
else:
|
|
rel_log.debug(message)
|
|
|
|
return "Internal"
|
|
|
|
def invoke_api(payload, url, auth, polls, begin="start/"):
|
|
""" Sends an API command request to the online IDE. Waits for a task completed
|
|
response before returning the results.
|
|
|
|
Args:
|
|
payload - Configuration parameters to be passed to the API
|
|
url - THe URL for the online compiler API
|
|
auth - Tuple containing authentication credentials
|
|
polls - Number of times to poll for results
|
|
begin - Default value = "start/", start command to be appended to URL
|
|
|
|
Returns:
|
|
result - True/False indicating the success/failure of the compilation
|
|
fail_type - the failure text if the compilation failed, else None
|
|
"""
|
|
|
|
# send task to api
|
|
rel_log.debug(url + begin + "| data: " + str(payload))
|
|
r = requests.post(url + begin, data=payload, auth=auth)
|
|
rel_log.debug(r.request.body)
|
|
|
|
if r.status_code != 200:
|
|
rel_log.error("HTTP code %d reported.", r.status_code)
|
|
return False, "Internal"
|
|
|
|
response = r.json()
|
|
rel_log.debug(response)
|
|
uuid = response['result']['data']['task_id']
|
|
rel_log.debug("Task accepted and given ID: %s", uuid)
|
|
result = False
|
|
fail_type = None
|
|
|
|
# It currently seems to take the onlide IDE API ~30s to process the compile
|
|
# request and provide a response. Set the poll time to half that in case it
|
|
# does manage to compile quicker.
|
|
poll_delay = 15
|
|
rel_log.debug("Running with a poll for response delay of: %ss", poll_delay)
|
|
|
|
# poll for output
|
|
for check in range(polls):
|
|
time.sleep(poll_delay)
|
|
|
|
try:
|
|
r = requests.get(url + "output/%s" % uuid, auth=auth)
|
|
|
|
except ConnectionError:
|
|
return "Internal"
|
|
|
|
response = r.json()
|
|
|
|
data = response['result']['data']
|
|
if data['task_complete']:
|
|
# Task completed. Now determine the result. Should be one of :
|
|
# 1) Successful compilation
|
|
# 2) Failed compilation with an error message
|
|
# 3) Internal failure of the online compiler
|
|
result = bool(data['compilation_success'])
|
|
if result:
|
|
rel_log.info("COMPILATION SUCCESSFUL\n")
|
|
else:
|
|
# Did this fail due to a genuine compilation error or a failue of
|
|
# the api itself ?
|
|
rel_log.info("COMPILATION FAILURE\n")
|
|
fail_type = get_compilation_failure(data['new_messages'])
|
|
break
|
|
else:
|
|
rel_log.info("COMPILATION FAILURE\n")
|
|
|
|
if not result and fail_type == None:
|
|
fail_type = "Internal"
|
|
|
|
return result, fail_type
|
|
|
|
|
|
def build_repo(target, program, user, pw, polls=25,
|
|
url="https://developer.mbed.org/api/v2/tasks/compiler/"):
|
|
""" Wrapper for sending an API command request to the online IDE. Sends a
|
|
build request.
|
|
|
|
Args:
|
|
target - Target to be built
|
|
program - Test program to build
|
|
user - mbed username
|
|
pw - mbed password
|
|
polls - Number of times to poll for results
|
|
url - THe URL for the online compiler API
|
|
|
|
Returns:
|
|
result - True/False indicating the success/failure of the compilation
|
|
fail_type - the failure text if the compilation failed, else None
|
|
"""
|
|
payload = {'clean':True, 'target':target, 'program':program}
|
|
auth = (user, pw)
|
|
return invoke_api(payload, url, auth, polls)
|
|
|
|
def run_cmd(command, exit_on_failure=False):
|
|
""" Passes a command to the system and returns a True/False result once the
|
|
command has been executed, indicating success/failure. Commands are passed
|
|
as a list of tokens.
|
|
E.g. The command 'git remote -v' would be passed in as ['git', 'remote', '-v']
|
|
|
|
Args:
|
|
command - system command as a list of tokens
|
|
exit_on_failure - If True exit the program on failure (default = False)
|
|
|
|
Returns:
|
|
result - True/False indicating the success/failure of the command
|
|
"""
|
|
rel_log.debug('[Exec] %s', ' '.join(command))
|
|
return_code = subprocess.call(command, shell=True)
|
|
|
|
if return_code:
|
|
rel_log.warning("The command '%s' failed with return code: %s",
|
|
(' '.join(command), return_code))
|
|
if exit_on_failure:
|
|
sys.exit(1)
|
|
|
|
return return_code
|
|
|
|
def run_cmd_with_output(command, exit_on_failure=False):
|
|
""" Passes a command to the system and returns a True/False result once the
|
|
command has been executed, indicating success/failure. If the command was
|
|
successful then the output from the command is returned to the caller.
|
|
Commands are passed as a list of tokens.
|
|
E.g. The command 'git remote -v' would be passed in as ['git', 'remote', '-v']
|
|
|
|
Args:
|
|
command - system command as a list of tokens
|
|
exit_on_failure - If True exit the program on failure (default = False)
|
|
|
|
Returns:
|
|
result - True/False indicating the success/failure of the command
|
|
output - The output of the command if it was successful, else empty string
|
|
"""
|
|
rel_log.debug('[Exec] %s', ' '.join(command))
|
|
returncode = 0
|
|
output = ""
|
|
try:
|
|
output = subprocess.check_output(command, shell=True)
|
|
except subprocess.CalledProcessError as e:
|
|
rel_log.warning("The command '%s' failed with return code: %s",
|
|
(' '.join(command), e.returncode))
|
|
returncode = e.returncode
|
|
if exit_on_failure:
|
|
sys.exit(1)
|
|
return returncode, output
|
|
|
|
def upgrade_test_repo(test, user, library, ref, repo_path):
|
|
""" Upgrades a local version of a test repo to the latest version of its
|
|
embedded library.
|
|
If the test repo is not present in the user area specified in the json
|
|
config file, then it will first be cloned.
|
|
Args:
|
|
test - Mercurial test repo name
|
|
user - Mercurial user name
|
|
library - library name
|
|
ref - SHA corresponding to the latest version of the library
|
|
repo_path - path to the user's repo area
|
|
|
|
Returns:
|
|
updated - True if library was updated, False otherwise
|
|
"""
|
|
rel_log.info("Updating test repo: '%s' to SHA: %s", test, ref)
|
|
cwd = os.getcwd()
|
|
|
|
repo = "https://" + user + '@developer.mbed.org/users/' + user + '/code/' + test
|
|
|
|
# Clone the repo if it doesn't already exist
|
|
path = abspath(repo_path + '/' + test)
|
|
if not os.path.exists(path):
|
|
rel_log.info("Test repo doesn't exist, cloning...")
|
|
os.chdir(abspath(repo_path))
|
|
clone_cmd = ['hg', 'clone', repo]
|
|
run_cmd(clone_cmd, exit_on_failure=True)
|
|
|
|
os.chdir(path)
|
|
|
|
client = hglib.open(path)
|
|
|
|
lib_file = library + '.lib'
|
|
if os.path.isfile(lib_file):
|
|
# Rename command will fail on some OS's if the target file already exist,
|
|
# so ensure if it does, it is deleted first.
|
|
bak_file = library + '_bak'
|
|
if os.path.isfile(bak_file):
|
|
os.remove(bak_file)
|
|
|
|
os.rename(lib_file, bak_file)
|
|
else:
|
|
rel_log.error("Failure to backup lib file prior to updating.")
|
|
return False
|
|
|
|
# mbed 2 style lib file contains one line with the following format
|
|
# e.g. https://developer.mbed.org/users/<user>/code/mbed-dev/#156823d33999
|
|
exp = 'https://developer.mbed.org/users/' + user + '/code/' + library + '/#[A-Za-z0-9]+'
|
|
lib_re = re.compile(exp)
|
|
updated = False
|
|
|
|
# Scan through mbed-os.lib line by line, looking for lib version and update
|
|
# it if found
|
|
with open(bak_file, 'r') as ip, open(lib_file, 'w') as op:
|
|
for line in ip:
|
|
|
|
opline = line
|
|
|
|
regexp = lib_re.match(line)
|
|
if regexp:
|
|
opline = 'https://developer.mbed.org/users/' + user + '/code/' + library + '/#' + ref
|
|
updated = True
|
|
|
|
op.write(opline)
|
|
|
|
if updated:
|
|
|
|
# Setup the default commit message
|
|
commit_message = '"Updating ' + library + ' to ' + ref + '"'
|
|
|
|
# Setup and run the commit command. Need to use the rawcommand in the hglib
|
|
# for this in order to pass the string value to the -m option. run_cmd using
|
|
# subprocess does not like this syntax.
|
|
try:
|
|
client.rawcommand(['commit','-m '+commit_message, lib_file])
|
|
|
|
cmd = ['hg', 'push', '-f', repo]
|
|
run_cmd(cmd, exit_on_failure=True)
|
|
|
|
except:
|
|
rel_log.info("Lib file already up to date and thus nothing to commit")
|
|
|
|
os.chdir(cwd)
|
|
return updated
|
|
|
|
def get_sha(repo_path, library):
|
|
""" Gets the latest SHA for the library specified. The library is assumed to be
|
|
located at the repo_path. If a SHA cannot be obtained this script will exit.
|
|
|
|
Args:
|
|
library - library name
|
|
repo_path - path to the user's repo area
|
|
|
|
Returns:
|
|
sha - last commit SHA
|
|
"""
|
|
cwd = os.getcwd()
|
|
sha = None
|
|
os.chdir(abspath(repo_path + '/' + library))
|
|
|
|
cmd = ['hg', 'log', '-l', '1']
|
|
ret, output = run_cmd_with_output(cmd, exit_on_failure=True)
|
|
|
|
# Output should contain a 4 line string of the form:
|
|
# changeset: 135:176b8275d35d
|
|
# tag: tip
|
|
# user: <>
|
|
# date: Thu Feb 02 16:02:30 2017 +0000
|
|
# summary: Release 135 of the mbed library
|
|
# All we want is the changeset string after version number
|
|
|
|
lines = output.split('\n')
|
|
fields = lines[0].split(':')
|
|
sha = fields[2]
|
|
|
|
os.chdir(cwd)
|
|
return sha
|
|
|
|
def get_latest_library_versions(repo_path):
|
|
""" Returns the latest library versions (SHAs) for 'mbed' and 'mbed-dev'.
|
|
If the SHAs cannot be obtained this script will exit.
|
|
|
|
Args:
|
|
repo_path - path to the user's repo area
|
|
|
|
Returns:
|
|
mbed - last commit SHA for mbed library
|
|
mbed_dev - last commit SHA for mbed_dev library
|
|
|
|
"""
|
|
|
|
mbed = get_sha(repo_path, 'mbed')
|
|
mbed_dev = get_sha(repo_path, 'mbed-dev')
|
|
|
|
return mbed, mbed_dev
|
|
|
|
def log_results(lst, title):
|
|
if len(lst) == 0:
|
|
rel_log.info("%s - None", title)
|
|
else:
|
|
for entry in lst:
|
|
rel_log.info("%s - Test: %s, Target: %s", title, entry[0], entry[1])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument('-l', '--log-level',
|
|
help="Level for providing logging output",
|
|
default='INFO')
|
|
args = parser.parse_args()
|
|
|
|
default = getattr(logging, 'INFO')
|
|
level = getattr(logging, args.log_level.upper(), default)
|
|
|
|
# Set logging level
|
|
logging.basicConfig(level=level)
|
|
rel_log = logging.getLogger("check-release")
|
|
|
|
# Read configuration data
|
|
with open(os.path.join(os.path.dirname(__file__), "check_release.json")) as config:
|
|
json_data = json.load(config)
|
|
|
|
supported_targets = []
|
|
|
|
if len(json_data["target_list"]) > 0:
|
|
# Compile user supplied subset of targets
|
|
supported_targets = json_data["target_list"]
|
|
else:
|
|
# Get a list of the officially supported mbed-os 2 targets
|
|
for tgt in OFFICIAL_MBED_LIBRARY_BUILD:
|
|
supported_targets.append(tgt[0])
|
|
|
|
ignore_list = []
|
|
|
|
if len(json_data["ignore_list"]) > 0:
|
|
# List of tuples of (test, target) to be ignored in this test
|
|
ignore_list = json_data["ignore_list"]
|
|
|
|
config = json_data["config"]
|
|
test_list = json_data["test_list"]
|
|
repo_path = config["mbed_repo_path"]
|
|
tests = []
|
|
|
|
# get username
|
|
cmd = ['hg', 'config', 'auth.x.username']
|
|
ret, output = run_cmd_with_output(cmd, exit_on_failure=True)
|
|
output = output.split('\n')
|
|
user = output[0]
|
|
|
|
# get password
|
|
cmd = ['hg', 'config', 'auth.x.password']
|
|
ret, output = run_cmd_with_output(cmd, exit_on_failure=True)
|
|
output = output.split('\n')
|
|
password = output[0]
|
|
|
|
mbed, mbed_dev = get_latest_library_versions(repo_path)
|
|
|
|
if not mbed or not mbed_dev:
|
|
rel_log.error("Could not obtain latest versions of library files!!")
|
|
exit(1)
|
|
|
|
rel_log.info("Latest mbed lib version = %s", mbed)
|
|
rel_log.info("Latest mbed-dev lib version = %s", mbed_dev)
|
|
|
|
# First update test repos to latest versions of their embedded libraries
|
|
for test in test_list:
|
|
tests.append(test['name'])
|
|
upgrade_test_repo(test['name'], user, test['lib'],
|
|
mbed if test['lib'] == "mbed" else mbed_dev,
|
|
repo_path)
|
|
|
|
total = len(supported_targets) * len(tests)
|
|
current = 0
|
|
retries = 10
|
|
passes = 0
|
|
failures = []
|
|
skipped = []
|
|
|
|
# Compile each test for each supported target
|
|
for test in tests:
|
|
for target in supported_targets:
|
|
|
|
combo = [test, target]
|
|
|
|
if combo in ignore_list:
|
|
rel_log.info("SKIPPING TEST: %s, TARGET: %s", test, target)
|
|
total -= 1
|
|
skipped.append(combo)
|
|
continue
|
|
|
|
current += 1
|
|
for retry in range(0, retries):
|
|
rel_log.info("COMPILING (%d/%d): TEST %s, TARGET: %s , attempt %u\n", current, total, test, target, retry)
|
|
result, mesg = build_repo(target, test, user, password)
|
|
if not result:
|
|
if mesg == 'Internal':
|
|
# Internal compiler error thus retry
|
|
continue
|
|
else:
|
|
# Actual error thus move on to next compilation
|
|
failures.append(combo)
|
|
break
|
|
|
|
passes += (int)(result)
|
|
break
|
|
else:
|
|
rel_log.error("Compilation failed due to internal errors.")
|
|
rel_log.error("Skipping test/target combination.")
|
|
total -= 1
|
|
skipped.append(combo)
|
|
|
|
rel_log.info(" SUMMARY OF COMPILATION RESULTS")
|
|
rel_log.info(" ------------------------------")
|
|
rel_log.info(" NUMBER OF TEST APPS: %d, NUMBER OF TARGETS: %d",
|
|
len(tests), len(supported_targets))
|
|
log_results(failures, " FAILED")
|
|
log_results(skipped, " SKIPPED")
|
|
|
|
# Output a % pass rate, indicate a failure if not 100% successful
|
|
pass_rate = (float(passes) / float(total)) * 100.0
|
|
rel_log.info(" PASS RATE %.1f %%\n", pass_rate)
|
|
sys.exit(not (pass_rate == 100))
|