diff --git a/tools/test/examples/examples.json b/tools/test/examples/examples.json index 0e7a99924eb..538017318a3 100644 --- a/tools/test/examples/examples.json +++ b/tools/test/examples/examples.json @@ -1,8 +1,109 @@ { - "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-blinky" : {}, - "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-mesh-minimal" : - {"features": ["IPV6"]}, - "https://github.com/ARMmbed/mbed-os-example-client" : {"features": ["IPV6"]}, - "https://github.com/ARMmbed/mbed-os-example-sockets" : {"features": ["IPV6"]}, - "https://github.com/ARMmbed/mbed-os-example-uvisor" : {"targets": ["K64F"], "toolchains":["GCC_ARM"]} -} + "examples": [ + { + "name": "mbed-os-example-blinky", + "github": "https://github.com/ARMmbed/mbed-os-example-blinky", + "mbed": [ + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-blinky" + ], + "features" : [], + "targets" : [], + "toolchains" : [], + "compile" : true, + "auto-update" : true + }, + { + "name": "mbed-os-example-tls", + "github": "https://github.com/ARMmbed/mbed-os-example-tls", + "mbed": [ + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-tls-benchmark", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-tls-tls-client", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-tls-hashing", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-tls-authcrypt" + ], + "features" : [], + "targets" : ["K64F", "NUCLEO_F429ZI"], + "toolchains" : ["GCC_ARM", "ARM"], + "compile" : true, + "auto-update" : true + }, + { + "name": "mbed-os-example-mesh-minimal", + "github":"https://github.com/ARMmbed/mbed-os-example-mesh-minimal", + "mbed": [ + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-mesh-minimal" + ], + "features" : ["IPV6"], + "targets" : [], + "toolchains" : [], + "compile" : true, + "auto-update" : true + }, + { + "name": "mbed-os-example-ble", + "github":"https://github.com/ARMmbed/mbed-os-example-ble", + "mbed": [ + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-Beacon", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-HeartRate", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-Thermometer", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-LEDBlinker", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-LED", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-GAPButton", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-EddystoneService", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-EddystoneObserver", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-Button", + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-ble-BatteryLevel" + ], + "features" : ["BLE"], + "targets" : ["NRF51_DK", "NRF52_DK", "K64F", "NUCLEO_F401RE"], + "toolchains" : [], + "compile" : true, + "auto-update" : true + }, + { + "name": "mbed-os-example-client", + "github":"https://github.com/ARMmbed/mbed-os-example-client", + "mbed": [ + "https://developer.mbed.org/teams/mbed-os-examples/code/mbed-os-example-client" + ], + "features" : ["IPV6"], + "targets" : [], + "toolchains" : [], + "compile" : true, + "auto-update" : true + }, + { + "name": "mbed-os-example-sockets", + "github":"https://github.com/ARMmbed/mbed-os-example-sockets", + "mbed": [ + ], + "features" : ["IPV6"], + "targets" : [], + "toolchains" : [], + "compile" : true, + "auto-update" : true + }, + { + "name": "mbed-os-example-wifi", + "github":"https://github.com/ARMmbed/mbed-os-example-wifi", + "mbed": [ + ], + "features" : [], + "targets" : [], + "toolchains" : [], + "compile" : false, + "auto-update" : true + }, + { + "name": "mbed-os-example-uvisor", + "github":"https://github.com/ARMmbed/mbed-os-example-uvisor", + "mbed": [ + ], + "features" : [], + "targets" : ["K64F"], + "toolchains" : ["GCC_ARM"], + "compile" : true, + "auto-update" : false + } + ] +} \ No newline at end of file diff --git a/tools/test/examples/examples.py b/tools/test/examples/examples.py index c06933f5bf5..fecadef4567 100644 --- a/tools/test/examples/examples.py +++ b/tools/test/examples/examples.py @@ -11,56 +11,21 @@ ROOT = abspath(dirname(dirname(dirname(dirname(__file__))))) sys.path.insert(0, ROOT) -from tools.build_api import get_mbed_official_release -from tools.targets import TARGET_MAP from tools.utils import argparse_force_uppercase_type - - -EXAMPLES = json.load(open(os.path.join(os.path.dirname(__file__), - "examples.json"))) - -def print_stuff(name, lst): - if lst: - print("#"*80) - print("# {} example combinations".format(name)) - print("#") - for thing in lst: - print(thing) - - -SUPPORTED_TOOLCHAINS = ["ARM", "IAR", "GCC_ARM"] - - -def target_cross_toolchain(allowed_toolchains, - features=[], targets=TARGET_MAP.keys(), - toolchains=SUPPORTED_TOOLCHAINS): - """Generate pairs of target and toolchains - - Args: - allowed_toolchains - a list of all possible toolchains - - Kwargs: - features - the features that must be in the features array of a - target - targets - a list of available targets - toolchains - a list of available toolchains - """ - for release_target, release_toolchains in get_mbed_official_release("5"): - for toolchain in release_toolchains: - if (toolchain in allowed_toolchains and - toolchain in toolchains and - release_target in targets and - all(feature in TARGET_MAP[release_target].features - for feature in features)): - yield release_target, toolchain +import examples_lib as lib +from examples_lib import SUPPORTED_TOOLCHAINS def main(): """Entry point""" parser = ArgumentParser() + parser.add_argument("-c", dest="config", default="examples.json") subparsers = parser.add_subparsers() import_cmd = subparsers.add_parser("import") import_cmd.set_defaults(fn=do_import) + version_cmd = subparsers.add_parser("tag") + version_cmd.add_argument("tag") + version_cmd.set_defaults(fn=do_versionning) compile_cmd = subparsers.add_parser("compile") compile_cmd.set_defaults(fn=do_compile) compile_cmd.add_argument( @@ -68,38 +33,32 @@ def main(): type=argparse_force_uppercase_type(SUPPORTED_TOOLCHAINS, "toolchain")) args = parser.parse_args() - return args.fn(args) + config = json.load(open(os.path.join(os.path.dirname(__file__), + args.config))) + return args.fn(args, config) -def do_import(_): + +def do_import(_, config): """Do the import step of this process""" - for example, _ in EXAMPLES.iteritems(): - subprocess.call(["mbed-cli", "import", example]) + lib.source_repos(config) return 0 - -def do_compile(args): +def do_compile(args, config): """Do the compile step""" - failures = [] - sucesses = [] - for example, requirements in EXAMPLES.iteritems(): - os.chdir(basename(example)) - for target, toolchain in target_cross_toolchain(args.toolchains, - **requirements): - proc = subprocess.Popen(["mbed-cli", "compile", "-t", toolchain, - "-m", target, "--silent"]) - proc.wait() - example_name = "{} {} {}".format(basename(example), target, - toolchain) - if proc.returncode: - failures.append(example_name) - else: - sucesses.append(example_name) - os.chdir("..") + results = {} + results = lib.compile_repos(config, args.toolchains) + + lib.print_compilation_summary(results) + failures = lib.get_num_failures(results) + print("Number of failures = %d" % failures) + return failures + +def do_versionning(args, config): + """ Test update the mbed-os to the version specified by the tag """ + lib.update_mbedos_version(config, args.tag) + return 0 - print_stuff("Passed", sucesses) - print_stuff("Failed", failures) - return len(failures) if __name__ == "__main__": sys.exit(main()) diff --git a/tools/test/examples/examples_lib.py b/tools/test/examples/examples_lib.py new file mode 100644 index 00000000000..ec34c416df9 --- /dev/null +++ b/tools/test/examples/examples_lib.py @@ -0,0 +1,217 @@ +""" Import and bulid a bunch of example programs + + This library includes functions that are shared between the examples.py and + the update.py modules. + + """ +import os +from os.path import dirname, abspath, basename +import os.path +import sys +import subprocess +from shutil import rmtree + +ROOT = abspath(dirname(dirname(dirname(dirname(__file__))))) +sys.path.insert(0, ROOT) + +from tools.build_api import get_mbed_official_release +from tools.targets import TARGET_MAP + +SUPPORTED_TOOLCHAINS = ["ARM", "IAR", "GCC_ARM"] + +def print_list(lst): + """Prints to screen the contents of a list + + Args: + lst - a list of any type, to be displayed + + """ + if lst: + for thing in lst: + print("# %s" % thing) + +def print_compilation_summary(results): + """Prints to screen the results of compiling combinations of example programs, + targets and compile chains. + + Args: + results - results of the compilation stage. See compile_repos() for + details of the format. + + """ + + print("#"*80) + print("# Examples compilation summary") + print("#"*80) + print("#") + print("# Passed example combinations") + print("#") + for key, val in results.iteritems(): + print_list(val[2]) + + print("#") + print("# Failed example combinations") + print("#") + for key, val in results.iteritems(): + print_list(val[3]) + print("#") + print("#"*80) + + +def target_cross_toolchain(allowed_toolchains, + features=[], targets=[]): + """Generate pairs of target and toolchains + + Args: + allowed_toolchains - a list of all possible toolchains + + Kwargs: + features - the features that must be in the features array of a + target + targets - a list of available targets + """ + if len(targets) == 0: + targets=TARGET_MAP.keys() + + for target, toolchains in get_mbed_official_release("5"): + for toolchain in toolchains: + if (toolchain in allowed_toolchains and + target in targets and + all(feature in TARGET_MAP[target].features + for feature in features)): + yield target, toolchain + + +def get_repo_list(example): + """ Returns a list of all the repos associated with the specific example in the json + config file. + If there are repos listed under the mbed section then these will be returned as a + list. If not then the github single repo with be returned. + NOTE: This does not currently deal with multiple examples underneath a github + sourced exampe repo. + + Args: + example - Example for which the repo list is requested + repos - The list of repos contained within that example in the json file + + """ + repos = [] + if len(example['mbed']) > 0: + for repo in example['mbed']: + repos.append(repo) + else: + repos.append(example['github']) + return repos + +def source_repos(config): + """ Clones each of the repos associated with the specific examples name from the + json config file. Note if there is already a clone of the repo then it will first + be removed to ensure a clean, up to date cloning. + Args: + config - the json object imported from the file. + + """ + print("\nImporting example repos....\n") + for example in config['examples']: + for repo in get_repo_list(example): + name = basename(repo) + if os.path.exists(name): + print("'%s' example directory already exists. Deleting..." % name) + rmtree(name) + + subprocess.call(["mbed-cli", "import", repo]) + +def get_num_failures(results): + """ Returns the number of failed compilations from the results summary + Args: + results - results summary of the compilation stage. See compile_repos() for + details of the format. + num_failures + + """ + num_failures = 0 + + for key, val in results.iteritems(): + num_failures = num_failures + len(val[3]) + + return num_failures + +def compile_repos(config, toolchains): + """Compiles combinations of example programs, targets and compile chains. + + The results are returned in a [key: value] dictionary format: + Where key = The example name from the json config file + value = a list containing: pass_status, successes, and failures + + where pass_status = The overall pass status for the compilation of the full + set of example programs comprising the example suite. + True if all examples pass, false otherwise + successes = list of passing examples. + failures = list of failing examples. + + Both successes and failures contain the example name, target and compile chain + + Args: + config - the json object imported from the file. + toolchains - List of toolchains to compile for. + results - results of the compilation stage. + + """ + results = {} + print("\nCompiling example repos....\n") + for example in config['examples']: + failures = [] + successes = [] + compiled = True + pass_status = True + if example['compile']: + if len(example['toolchains']) > 0: + toolchains = example['toolchains'] + + for repo in get_repo_list(example): + os.chdir(basename(repo)) + + # Check that the target, toolchain and features combinations are valid and return a + # list of valid combinations to work through + for target, toolchain in target_cross_toolchain(toolchains, + example['features'], example['targets']): + proc = subprocess.Popen(["mbed-cli", "compile", "-t", toolchain, + "-m", target, "--silent"]) + proc.wait() + example_summary = "{} {} {}".format(basename(repo), target, toolchain) + if proc.returncode: + failures.append(example_summary) + else: + successes.append(example_summary) + os.chdir("..") + + # If there are any compilation failures for the example 'set' then the overall status is fail. + if len(failures) > 0: + pass_status = False + else: + compiled = False + + results[example['name']] = [compiled, pass_status, successes, failures] + + return results + + +def update_mbedos_version(config, tag): + """ For each example repo identified in the config json object, update the version of + mbed-os to that specified by the supplied GitHub tag. This function assumes that each + example repo has already been cloned. + + Args: + config - the json object imported from the file. + tag - GitHub tag corresponding to a version of mbed-os to upgrade to. + + """ + print("Updating mbed-os in examples to version %s\n" % tag) + for example in config['examples']: + for repo in get_repo_list(example): + update_dir = basename(repo) + "/mbed-os" + print("\nChanging dir to %s\n" % update_dir) + os.chdir(update_dir) + subprocess.call(["mbed-cli", "update", tag, "--clean"]) + os.chdir("../..") + diff --git a/tools/test/examples/update.py b/tools/test/examples/update.py new file mode 100644 index 00000000000..404e56e6f37 --- /dev/null +++ b/tools/test/examples/update.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python + +import os +from os.path import dirname, abspath, basename +import sys +import argparse +import json +import subprocess +import shutil +import stat + +ROOT = abspath(dirname(dirname(dirname(dirname(__file__))))) +sys.path.insert(0, ROOT) + +import examples_lib as lib +from examples_lib import SUPPORTED_TOOLCHAINS + +def run_cmd(command, print_warning_on_fail=True): + """ Takes the command specified and runs it in a sub-process, obtaining the return code. + + Args: + command - command to run, provided as a list of individual fields which are combined into a + single command before passing to the sub-process call. + return_code - result of the command. + + """ + print('[Exec] %s' % ' '.join(command)) + return_code = subprocess.call(command) + + if return_code: + print("The command '%s' failed with return code: %s" % (' '.join(command), return_code)) + print("Ignoring and moving on to the next example") + + return return_code + + +def rmtree_readonly(directory): + """ Deletes a readonly directory tree. + + Args: + directory - tree to delete + """ + def remove_readonly(func, path, _): + os.chmod(path, stat.S_IWRITE) + func(path) + + shutil.rmtree(directory, onerror=remove_readonly) + +def find_all_examples(path): + """ Searches the path specified for sub-example folders, ie those containing an + mbed-os.lib file. If found adds the path to the sub-example to a list which is + then returned. + + Args: + path - path to search. + examples - (returned) list of paths to example directories. + + """ + examples = [] + for root, dirs, files in os.walk(path): + if 'mbed-os.lib' in files: + examples += [root] + + return examples + +def upgrade_single_example(example, tag, directory): + """ Updates the mbed-os.lib file in the example specified to correspond to the + version specified by the GitHub tag supplied. Also deals with + multiple sub-examples in the GitHub repo, updating them in the same way. + + Args: + example - json example object containing the GitHub repo to update. + tag - GitHub tag corresponding to a version of mbed-os to upgrade to. + directory - directory path for the example. + returns - True if the upgrade was successful, False otherwise. + + """ + print("Upgrading single example at path '%s'" % directory) + cwd = os.getcwd() + os.chdir(directory) + + return_code = None + + # Change directories to the mbed-os library + if not os.path.exists('mbed-os'): + print("'mbed-os' directory not found in the root of '%s'" % directory) + print("Ignoring and moving on to the next example") + os.chdir(cwd) + return False + + os.chdir('mbed-os') + + # Setup and run the update command + update_cmd = ['mbed', 'update', tag] + return_code = run_cmd(update_cmd) + + if return_code: + os.chdir(cwd) + return False + + os.chdir('../') + + # Setup and run the add command + add_cmd = ['git', 'add', 'mbed-os.lib'] + return_code = run_cmd(add_cmd) + + os.chdir(cwd) + return not return_code + +def upgrade_example(example, tag): + """ Clones the example specified from GitHub and updates the associated mbed-os.lib file + to correspond to the version specified by the GitHub tag supplied. Also deals with + multiple sub-examples in the GitHub repo, updating them in the same way. + + Args: + example - json example object containing the GitHub repo to update. + tag - GitHub tag corresponding to a version of mbed-os to upgrade to. + + """ + print("Updating example '%s'" % example['name']) + cwd = os.getcwd() + + # Setup and run the import command + clone_cmd = ['git', 'clone', example['github']] + return_code = run_cmd(clone_cmd) + + if return_code: + return False + + # Find all examples + example_directories = find_all_examples(example['name']) + + os.chdir(example['name']) + + # Setup and run the update command + import_cmd = ['mbed', 'update'] + return_code = run_cmd(import_cmd) + if return_code: + os.chdir(cwd) + return False + + for example_directory in example_directories: + if not upgrade_single_example(example, tag, os.path.relpath(example_directory, example['name'])): + os.chdir(cwd) + return False + + # Setup the default commit message + commit_message = 'Updating mbed-os to {{' + tag +'}}' + + # Setup and run the commit command + commit_cmd = ['git', 'commit', '-m', commit_message] + return_code = run_cmd(commit_cmd) + if return_code: + if return_code == 1: + print("[WARNING] 'git commit' exited with a return code of 1. " + \ + "This usually inidicates that no update was made. Still " + \ + "attempting to create a tag.") + else: + os.chdir(cwd) + return False + + # Setup and run the tag command + tag_cmd = ['git', 'tag', '-a', tag, '-m', tag] + return_code = run_cmd(tag_cmd) + if return_code: + os.chdir(cwd) + return False + + # Setup and run the push command + push_cmd = ['git', 'push', 'origin', 'master'] + return_code = run_cmd(push_cmd) + + if return_code: + os.chdir(cwd) + return False + + push_cmd = ['git', 'push', 'origin', tag] + return_code = run_cmd(push_cmd) + + os.chdir(cwd) + return not return_code + +def create_work_directory(path): + """ Create a new directory specified in 'path', overwrite if the directory already + exists. + + Args: + path - directory path to be created. + + """ + if os.path.exists(path): + print("'%s' directory already exists. Deleting..." % path) + rmtree_readonly(path) + + os.makedirs(path) + +def test_compile(config, tag): + """ For each example repo identified in the config json object, clone, update mbed-os to + the specified tag and then compile for all supported toolchains. + + Args: + config - the json object imported from the file. + tag - GitHub tag corresponding to a version of mbed-os to upgrade to. + results - summary of compilation results. + + """ + # Create work directories + create_work_directory('test_compile') + + # Loop through the examples + results = {} + os.chdir('test_compile') + + lib.source_repos(config) + lib.update_mbedos_version(config, tag) + results = lib.compile_repos(config, SUPPORTED_TOOLCHAINS) + os.chdir("..") + + return results + + +def main(arguments): + + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('tag', help="mbed-os tag to which all examples will be updated") + parser.add_argument('-c', '--config_file', help="Path to the configuration file (default is 'examples.json')", default='examples.json') + + args = parser.parse_args(arguments) + + cfg = os.path.join(os.path.dirname(__file__), args.config_file) + + # Load the config file + config = json.load(open(os.path.join(os.path.dirname(__file__), + args.config_file))) + + if not config: + print("Failed to load config file '%s'" % args.config_file) + sys.exit(1) + + # Create work directories + create_work_directory('examples') + + # Loop through the examples + failures = [] + successes = [] + not_compiled = [] + results = {} + os.chdir('examples') + + results = test_compile(config, args.tag) + lib.print_compilation_summary(results) + + for example in config['examples']: + # Determine if this example should be updated + + # Attempt to update if: + # group of examples passed compilation and + # auto update is set to True + # Note: results fields are [compiled flag, pass flag, successes list, failures list] + if not results[example['name']][0]: + # Example was not compiled + not_compiled += [example['name']] + else: + if results[example['name']][1] and example['auto-update']: + if upgrade_example(example, args.tag): + successes += [example['name']] + else: + failures += [example['name']] + else: + failures += [example['name']] + + os.chdir('../') + + # Finish the script and report the results + print(os.linesep + os.linesep +'Finished updating examples!' + os.linesep) + + if successes: + print('The following examples updated successfully:') + for success in successes: + print(' - %s' % success) + + if failures: + print('\nThe following examples were not updated:') + for fail in failures: + print(' - %s' % fail) + + if not_compiled: + print('The following examples were skipped:') + for example in not_compiled: + print(' - %s' % example) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) \ No newline at end of file