did_i_break_something.py
1 """ 2 Simple script to see if everything is building properly. 3 Ideally, this script will be run before pushing changes to github. 4 Eventually, a much nicer version of this will be used for CI 5 6 Usage: python3 did_i_break_something.py 7 """ 8 9 import subprocess 10 import tempfile 11 import argparse 12 import sys 13 import os 14 import git 15 import yaml 16 import glob 17 18 19 def get_project_root(): 20 git_repo = git.Repo(search_parent_directories=True) 21 return git_repo.git.rev_parse("--show-toplevel") 22 23 24 # Get commands to build firmware project using test arguments 25 def build_fw_commands(test, verbose=False, jobs=4): 26 project_root = get_project_root() 27 app = test["args"]["app"] 28 bsp = test["args"]["bsp"] 29 build_type = test["args"]["build_type"] 30 31 build_cmd = f"cmake {project_root} -DCMAKE_TOOLCHAIN_FILE={project_root}/cmake/arm-none-eabi-gcc.cmake -DBSP={bsp} -DAPP={app} -DCMAKE_BUILD_TYPE={build_type}" 32 33 if "use_bootloader" in test["args"] and test["args"]["use_bootloader"]: 34 build_cmd += " -DUSE_BOOTLOADER=1" 35 36 if "defines" in test["args"]: 37 build_cmd += " " + test["args"]["defines"] 38 39 make_cmd = f"make -j {jobs} CC=gcc" 40 if verbose: 41 make_cmd += " VERBOSE=1" 42 43 commands = [build_cmd.split(" "), make_cmd.split(" ")] 44 45 return commands 46 47 48 # Get commands to build and run unit tests 49 def unit_test_commands(test, verbose=False, jobs=4): 50 project_root = get_project_root() 51 52 cmake_cmd = f"cmake {project_root}" 53 ctest_cmd = "ctest -V" 54 55 commands = [cmake_cmd.split(" "), f"make -j {jobs}".split(" "), ctest_cmd.split(" ")] 56 57 return commands 58 59 60 # Map configuration types to functions to get commands to run 61 test_handlers = {"build_fw": build_fw_commands, "unit_tests": unit_test_commands} 62 63 64 def run_test(test, verbose=False): 65 returncode = 0 66 67 # Tests are run in temporary directory inside repository 68 # This is so scripts that use/reference git will work (like version.c) 69 with tempfile.TemporaryDirectory( 70 dir=get_project_root(), prefix="tmpbuild_" 71 ) as tmpdirname: 72 previous_dir = os.getcwd() 73 os.chdir(tmpdirname) 74 75 if test["type"] not in test_handlers: 76 raise NotImplementedError("Unsupported test type!") 77 78 # Get all commands for specific test 79 commands = test_handlers[test["type"]](test, verbose) 80 81 for command in commands: 82 if verbose: 83 print("Running ", " ".join(command)) 84 result = subprocess.run(command, capture_output=True, text=True) 85 if result.returncode != 0: 86 print("\nSTDOUT:\n", result.stdout) 87 print("\nSTDERR:\n", result.stderr) 88 print("return code:", result.returncode) 89 returncode = result.returncode 90 break 91 92 # Go back to where we started before the temp dir gets deleted 93 os.chdir(previous_dir) 94 95 return returncode 96 97 98 parser = argparse.ArgumentParser() 99 parser.add_argument("--verbose", action="store_true", help="Print more stuff") 100 parser.add_argument( 101 "configs", 102 nargs="+", 103 help="Configuration (yaml) files with build/test configuration. Can also be directory with config files", 104 ) 105 args = parser.parse_args() 106 107 108 # TODO - gather build sizes, etc... 109 110 config_paths = [] 111 for config in args.configs: 112 if os.path.isdir(config): 113 # If it's a directory, load all the yaml files 114 config_paths += sorted(glob.glob(os.path.join(config, "*.yaml"))) 115 elif os.path.exists(config): 116 # If it's a file, make sure it exists 117 config_paths.append(config) 118 else: 119 raise FileNotFoundError(f"{config} not found!") 120 121 tests = [] 122 for path in config_paths: 123 with open(path, "r") as config: 124 # TODO - validate config file 125 tests += yaml.safe_load(config) 126 127 # Run through each test and see if it fails or not 128 for test in tests: 129 print(f'Building: {test["name"]}') 130 returncode = run_test(test, args.verbose) 131 if returncode == 0: 132 print("PASS") 133 else: 134 print("FAIL") 135 # TODO - maybe run all the tests before exiting? 136 sys.exit(returncode)