/ tools / scripts / test / did_i_break_something.py
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)