idf_ext.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import glob
  2. import tempfile
  3. import os
  4. import os.path
  5. import re
  6. import shutil
  7. import argparse
  8. import copy
  9. PROJECT_NAME = "unit-test-app"
  10. PROJECT_PATH = os.getcwd()
  11. # List of unit-test-app configurations.
  12. # Each file in configs/ directory defines a configuration. The format is the
  13. # same as sdkconfig file. Configuration is applied on top of sdkconfig.defaults
  14. # file from the project directory
  15. CONFIG_NAMES = os.listdir(os.path.join(PROJECT_PATH, "configs"))
  16. # Build (intermediate) and output (artifact) directories
  17. BUILDS_DIR = os.path.join(PROJECT_PATH, "builds")
  18. BINARIES_DIR = os.path.join(PROJECT_PATH, "output")
  19. # Convert the values passed to the -T parameter to corresponding cache entry definitions
  20. # TESTS_ALL and TEST_COMPONENTS
  21. class TestComponentAction(argparse.Action):
  22. def __call__(self, parser, namespace, values, option_string=None):
  23. # Create a new of cache definition entry, adding previous elements
  24. cache_entries = list()
  25. existing_entries = getattr(namespace, "define_cache_entry", [])
  26. if existing_entries:
  27. cache_entries.extend(existing_entries)
  28. # Form -D arguments
  29. if "all" in values:
  30. cache_entries.append("TESTS_ALL=1")
  31. cache_entries.append("TEST_COMPONENTS=''")
  32. else:
  33. cache_entries.append("TESTS_ALL=0")
  34. cache_entries.append("TEST_COMPONENTS='%s'" % " ".join(values))
  35. setattr(namespace, "define_cache_entry", cache_entries)
  36. # Brute force add reconfigure at the very beginning
  37. existing_actions = getattr(namespace, "actions", [])
  38. if "reconfigure" not in existing_actions:
  39. existing_actions = ["reconfigure"] + existing_actions
  40. setattr(namespace, "actions", existing_actions)
  41. class TestExcludeComponentAction(argparse.Action):
  42. def __call__(self, parser, namespace, values, option_string=None):
  43. # Create a new of cache definition entry, adding previous elements
  44. cache_entries = list()
  45. existing_entries = getattr(namespace, "define_cache_entry", [])
  46. if existing_entries:
  47. cache_entries.extend(existing_entries)
  48. cache_entries.append("TEST_EXCLUDE_COMPONENTS='%s'" % " ".join(values))
  49. setattr(namespace, "define_cache_entry", cache_entries)
  50. # Brute force add reconfigure at the very beginning
  51. existing_actions = getattr(namespace, "actions", [])
  52. if "reconfigure" not in existing_actions:
  53. existing_actions = ["reconfigure"] + existing_actions
  54. setattr(namespace, "actions", existing_actions)
  55. def add_argument_extensions(parser):
  56. # For convenience, define a -T argument that gets converted to -D arguments
  57. parser.add_argument('-T', '--test-component', help="Specify the components to test", nargs='+', action=TestComponentAction)
  58. # For convenience, define a -T argument that gets converted to -D arguments
  59. parser.add_argument('-E', '--test-exclude-components', help="Specify the components to exclude from testing", nargs='+', action=TestExcludeComponentAction)
  60. def add_action_extensions(base_functions, base_actions):
  61. def ut_apply_config(ut_apply_config_name, args):
  62. config_name = re.match(r"ut-apply-config-(.*)", ut_apply_config_name).group(1)
  63. def set_config_build_variables(prop, defval=None):
  64. property_value = re.findall(r"^%s=(.+)" % prop, config_file_content, re.MULTILINE)
  65. if (property_value):
  66. property_value = property_value[0]
  67. else:
  68. property_value = defval
  69. if (property_value):
  70. try:
  71. args.define_cache_entry.append("%s=" % prop + property_value)
  72. except AttributeError:
  73. args.define_cache_entry = ["%s=" % prop + property_value]
  74. return property_value
  75. sdkconfig_set = None
  76. if args.define_cache_entry:
  77. sdkconfig_set = filter(lambda s: "SDKCONFIG=" in s, args.define_cache_entry)
  78. sdkconfig_path = os.path.join(args.project_dir, "sdkconfig")
  79. if sdkconfig_set:
  80. sdkconfig_path = sdkconfig_set[-1].split("=")[1]
  81. sdkconfig_path = os.path.abspath(sdkconfig_path)
  82. try:
  83. os.remove(sdkconfig_path)
  84. except OSError:
  85. pass
  86. if config_name in CONFIG_NAMES:
  87. # Parse the sdkconfig for components to be included/excluded and tests to be run
  88. config = os.path.join(PROJECT_PATH, "configs", config_name)
  89. with open(config, "r") as config_file:
  90. config_file_content = config_file.read()
  91. set_config_build_variables("EXCLUDE_COMPONENTS", "''")
  92. test_components = set_config_build_variables("TEST_COMPONENTS", "''")
  93. tests_all = None
  94. if test_components == "''":
  95. tests_all = "TESTS_ALL=1"
  96. else:
  97. tests_all = "TESTS_ALL=0"
  98. try:
  99. args.define_cache_entry.append(tests_all)
  100. except AttributeError:
  101. args.define_cache_entry = [tests_all]
  102. set_config_build_variables("TEST_EXCLUDE_COMPONENTS","''")
  103. with tempfile.NamedTemporaryFile(delete=False) as sdkconfig_temp:
  104. # Use values from the combined defaults and the values from
  105. # config folder to build config
  106. sdkconfig_default = os.path.join(PROJECT_PATH, "sdkconfig.defaults")
  107. with open(sdkconfig_default, "rb") as sdkconfig_default_file:
  108. sdkconfig_temp.write(sdkconfig_default_file.read())
  109. sdkconfig_config = os.path.join(PROJECT_PATH, "configs", config_name)
  110. with open(sdkconfig_config, "rb") as sdkconfig_config_file:
  111. sdkconfig_temp.write(b"\n")
  112. sdkconfig_temp.write(sdkconfig_config_file.read())
  113. try:
  114. try:
  115. args.define_cache_entry.append("SDKCONFIG_DEFAULTS=" + sdkconfig_temp.name)
  116. except AttributeError:
  117. args.define_cache_entry = ["SDKCONFIG_DEFAULTS=" + sdkconfig_temp.name]
  118. reconfigure = base_functions["reconfigure"]
  119. reconfigure(None, args)
  120. finally:
  121. try:
  122. os.unlink(sdkconfig_temp.name)
  123. except OSError:
  124. pass
  125. else:
  126. if not config_name == "all-configs":
  127. print("unknown unit test app config for action '%s'" % ut_apply_config_name)
  128. # This target builds the configuration. It does not currently track dependencies,
  129. # but is good enough for CI builds if used together with clean-all-configs.
  130. # For local builds, use 'apply-config-NAME' target and then use normal 'all'
  131. # and 'flash' targets.
  132. def ut_build(ut_build_name, args):
  133. # Create a copy of the passed arguments to prevent arg modifications to accrue if
  134. # all configs are being built
  135. build_args = copy.copy(args)
  136. config_name = re.match(r"ut-build-(.*)", ut_build_name).group(1)
  137. if config_name in CONFIG_NAMES:
  138. build_args.build_dir = os.path.join(BUILDS_DIR, config_name)
  139. src = os.path.join(BUILDS_DIR, config_name)
  140. dest = os.path.join(BINARIES_DIR, config_name)
  141. try:
  142. os.makedirs(dest)
  143. except OSError:
  144. pass
  145. # Build, tweaking paths to sdkconfig and sdkconfig.defaults
  146. ut_apply_config("ut-apply-config-" + config_name, build_args)
  147. build_target = base_functions["build_target"]
  148. build_target("all", build_args)
  149. # Copy artifacts to the output directory
  150. shutil.copyfile(os.path.join(build_args.project_dir, "sdkconfig"), os.path.join(dest, "sdkconfig"))
  151. binaries = [PROJECT_NAME + x for x in [".elf", ".bin", ".map"]]
  152. for binary in binaries:
  153. shutil.copyfile(os.path.join(src, binary), os.path.join(dest, binary))
  154. try:
  155. os.mkdir(os.path.join(dest, "bootloader"))
  156. except OSError:
  157. pass
  158. shutil.copyfile(os.path.join(src, "bootloader", "bootloader.bin"), os.path.join(dest, "bootloader", "bootloader.bin"))
  159. for partition_table in glob.glob(os.path.join(src, "partition_table", "partition-table*.bin")):
  160. try:
  161. os.mkdir(os.path.join(dest, "partition_table"))
  162. except OSError:
  163. pass
  164. shutil.copyfile(partition_table, os.path.join(dest, "partition_table", os.path.basename(partition_table)))
  165. shutil.copyfile(os.path.join(src, "flasher_args.json"), os.path.join(dest, "flasher_args.json"))
  166. binaries = glob.glob(os.path.join(src, "*.bin"))
  167. binaries = [os.path.basename(s) for s in binaries]
  168. for binary in binaries:
  169. shutil.copyfile(os.path.join(src, binary), os.path.join(dest, binary))
  170. else:
  171. if not config_name == "all-configs":
  172. print("unknown unit test app config for action '%s'" % ut_build_name)
  173. def ut_clean(ut_clean_name, args):
  174. config_name = re.match(r"ut-clean-(.*)", ut_clean_name).group(1)
  175. if config_name in CONFIG_NAMES:
  176. shutil.rmtree(os.path.join(BUILDS_DIR, config_name), ignore_errors=True)
  177. shutil.rmtree(os.path.join(BINARIES_DIR, config_name), ignore_errors=True)
  178. else:
  179. if not config_name == "all-configs":
  180. print("unknown unit test app config for action '%s'" % ut_clean_name)
  181. def ut_help(action, args):
  182. HELP_STRING = """
  183. Additional unit-test-app specific targets
  184. idf.py ut-build-NAME - Build unit-test-app with configuration provided in configs/NAME.
  185. Build directory will be builds/NAME/, output binaries will be
  186. under output/NAME/
  187. idf.py ut-clean-NAME - Remove build and output directories for configuration NAME.
  188. idf.py ut-build-all-configs - Build all configurations defined in configs/ directory.
  189. idf.py ut-apply-config-NAME - Generates configuration based on configs/NAME in sdkconfig
  190. file. After this, normal all/flash targets can be used.
  191. Useful for development/debugging.
  192. """
  193. print(HELP_STRING)
  194. # Build dictionary of action extensions
  195. extensions = dict()
  196. # This generates per-config targets (clean, build, apply-config).
  197. build_all_config_deps = []
  198. clean_all_config_deps = []
  199. for config in CONFIG_NAMES:
  200. config_build_action_name = "ut-build-" + config
  201. config_clean_action_name = "ut-clean-" + config
  202. config_apply_config_action_name = "ut-apply-config-" + config
  203. extensions[config_build_action_name] = (ut_build, [], [])
  204. extensions[config_clean_action_name] = (ut_clean, [], [])
  205. extensions[config_apply_config_action_name] = (ut_apply_config, [], [])
  206. build_all_config_deps.append(config_build_action_name)
  207. clean_all_config_deps.append(config_clean_action_name)
  208. extensions["ut-build-all-configs"] = (ut_build, build_all_config_deps, [])
  209. extensions["ut-clean-all-configs"] = (ut_clean, clean_all_config_deps, [])
  210. extensions["ut-help"] = (ut_help, [], [])
  211. base_actions.update(extensions)