IDFApp.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. # Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http:#www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """ IDF Test Applications """
  15. import hashlib
  16. import json
  17. import os
  18. import re
  19. import subprocess
  20. import sys
  21. from abc import abstractmethod
  22. from tiny_test_fw import App
  23. from .IDFAssignTest import ExampleGroup, TestAppsGroup, UnitTestGroup, IDFCaseGroup
  24. try:
  25. import gitlab_api
  26. except ImportError:
  27. gitlab_api = None
  28. def parse_flash_settings(path):
  29. file_name = os.path.basename(path)
  30. if file_name == "flasher_args.json":
  31. # CMake version using build metadata file
  32. with open(path, "r") as f:
  33. args = json.load(f)
  34. flash_files = [(offs, binary) for (offs, binary) in args["flash_files"].items() if offs != ""]
  35. flash_settings = args["flash_settings"]
  36. app_name = os.path.splitext(args["app"]["file"])[0]
  37. else:
  38. # GNU Make version uses download.config arguments file
  39. with open(path, "r") as f:
  40. args = f.readlines()[-1].split(" ")
  41. flash_files = []
  42. flash_settings = {}
  43. for idx in range(0, len(args), 2): # process arguments in pairs
  44. if args[idx].startswith("--"):
  45. # strip the -- from the command line argument
  46. flash_settings[args[idx][2:]] = args[idx + 1]
  47. else:
  48. # offs, filename
  49. flash_files.append((args[idx], args[idx + 1]))
  50. # we can only guess app name in download.config.
  51. for p in flash_files:
  52. if not os.path.dirname(p[1]) and "partition" not in p[1]:
  53. # app bin usually in the same dir with download.config and it's not partition table
  54. app_name = os.path.splitext(p[1])[0]
  55. break
  56. else:
  57. app_name = None
  58. return flash_files, flash_settings, app_name
  59. class Artifacts(object):
  60. def __init__(self, dest_root_path, artifact_index_file, app_path, config_name, target):
  61. assert gitlab_api
  62. # at least one of app_path or config_name is not None. otherwise we can't match artifact
  63. assert app_path or config_name
  64. assert os.path.exists(artifact_index_file)
  65. self.gitlab_inst = gitlab_api.Gitlab(os.getenv("CI_PROJECT_ID"))
  66. self.dest_root_path = dest_root_path
  67. with open(artifact_index_file, "r") as f:
  68. artifact_index = json.load(f)
  69. self.artifact_info = self._find_artifact(artifact_index, app_path, config_name, target)
  70. @staticmethod
  71. def _find_artifact(artifact_index, app_path, config_name, target):
  72. for artifact_info in artifact_index:
  73. match_result = True
  74. if app_path:
  75. match_result = app_path in artifact_info["app_dir"]
  76. if config_name:
  77. match_result = match_result and config_name == artifact_info["config"]
  78. if target:
  79. match_result = match_result and target == artifact_info["target"]
  80. if match_result:
  81. ret = artifact_info
  82. break
  83. else:
  84. ret = None
  85. return ret
  86. def _get_app_base_path(self):
  87. if self.artifact_info:
  88. return os.path.join(self.artifact_info["work_dir"], self.artifact_info["build_dir"])
  89. else:
  90. return None
  91. def _get_flash_arg_file(self, base_path, job_id):
  92. if self.artifact_info["build_system"] == "cmake":
  93. flash_arg_file = os.path.join(base_path, "flasher_args.json")
  94. else:
  95. flash_arg_file = os.path.join(base_path, "download.config")
  96. self.gitlab_inst.download_artifact(job_id, [flash_arg_file], self.dest_root_path)
  97. return flash_arg_file
  98. def _download_binary_files(self, base_path, job_id, flash_arg_file):
  99. flash_files, flash_settings, app_name = parse_flash_settings(os.path.join(self.dest_root_path,
  100. flash_arg_file))
  101. artifact_files = [os.path.join(base_path, p[1]) for p in flash_files]
  102. artifact_files.append(os.path.join(base_path, app_name + ".elf"))
  103. self.gitlab_inst.download_artifact(job_id, artifact_files, self.dest_root_path)
  104. def _download_sdkconfig_file(self, base_path, job_id):
  105. self.gitlab_inst.download_artifact(job_id, [os.path.join(os.path.dirname(base_path), "sdkconfig")],
  106. self.dest_root_path)
  107. def download_artifacts(self):
  108. if not self.artifact_info:
  109. return None
  110. base_path = self._get_app_base_path()
  111. job_id = self.artifact_info["ci_job_id"]
  112. # 1. download flash args file
  113. flash_arg_file = self._get_flash_arg_file(base_path, job_id)
  114. # 2. download all binary files
  115. self._download_binary_files(base_path, job_id, flash_arg_file)
  116. # 3. download sdkconfig file
  117. self._download_sdkconfig_file(base_path, job_id)
  118. return base_path
  119. def download_artifact_files(self, file_names):
  120. if self.artifact_info:
  121. base_path = os.path.join(self.artifact_info["work_dir"], self.artifact_info["build_dir"])
  122. job_id = self.artifact_info["ci_job_id"]
  123. # download all binary files
  124. artifact_files = [os.path.join(base_path, fn) for fn in file_names]
  125. self.gitlab_inst.download_artifact(job_id, artifact_files, self.dest_root_path)
  126. # download sdkconfig file
  127. self.gitlab_inst.download_artifact(job_id, [os.path.join(os.path.dirname(base_path), "sdkconfig")],
  128. self.dest_root_path)
  129. else:
  130. base_path = None
  131. return base_path
  132. class UnitTestArtifacts(Artifacts):
  133. BUILDS_DIR_RE = re.compile(r'^builds/')
  134. def _get_app_base_path(self):
  135. if self.artifact_info:
  136. output_dir = self.BUILDS_DIR_RE.sub('output/', self.artifact_info["build_dir"])
  137. return os.path.join(self.artifact_info["app_dir"], output_dir)
  138. else:
  139. return None
  140. def _download_sdkconfig_file(self, base_path, job_id):
  141. self.gitlab_inst.download_artifact(job_id, [os.path.join(base_path, "sdkconfig")], self.dest_root_path)
  142. class IDFApp(App.BaseApp):
  143. """
  144. Implements common esp-idf application behavior.
  145. idf applications should inherent from this class and overwrite method get_binary_path.
  146. """
  147. IDF_DOWNLOAD_CONFIG_FILE = "download.config"
  148. IDF_FLASH_ARGS_FILE = "flasher_args.json"
  149. def __init__(self, app_path, config_name=None, target=None, case_group=IDFCaseGroup, artifact_cls=Artifacts):
  150. super(IDFApp, self).__init__(app_path)
  151. self.app_path = app_path
  152. self.config_name = config_name
  153. self.target = target
  154. self.idf_path = self.get_sdk_path()
  155. self.case_group = case_group
  156. self.artifact_cls = artifact_cls
  157. self.binary_path = self.get_binary_path()
  158. self.elf_file = self._get_elf_file_path()
  159. self._elf_file_sha256 = None
  160. assert os.path.exists(self.binary_path)
  161. if self.IDF_DOWNLOAD_CONFIG_FILE not in os.listdir(self.binary_path):
  162. if self.IDF_FLASH_ARGS_FILE not in os.listdir(self.binary_path):
  163. msg = ("Neither {} nor {} exists. "
  164. "Try to run 'make print_flash_cmd | tail -n 1 > {}/{}' "
  165. "or 'idf.py build' "
  166. "for resolving the issue."
  167. "").format(self.IDF_DOWNLOAD_CONFIG_FILE, self.IDF_FLASH_ARGS_FILE,
  168. self.binary_path, self.IDF_DOWNLOAD_CONFIG_FILE)
  169. raise AssertionError(msg)
  170. self.flash_files, self.flash_settings = self._parse_flash_download_config()
  171. self.partition_table = self._parse_partition_table()
  172. def __str__(self):
  173. parts = ['app<{}>'.format(self.app_path)]
  174. if self.config_name:
  175. parts.extend('config<{}>'.format(self.config_name))
  176. if self.target:
  177. parts.extend('target<{}>'.format(self.target))
  178. return ' '.join(parts)
  179. @classmethod
  180. def get_sdk_path(cls): # type: () -> str
  181. idf_path = os.getenv("IDF_PATH")
  182. assert idf_path
  183. assert os.path.exists(idf_path)
  184. return idf_path
  185. def _get_sdkconfig_paths(self):
  186. """
  187. returns list of possible paths where sdkconfig could be found
  188. Note: could be overwritten by a derived class to provide other locations or order
  189. """
  190. return [os.path.join(self.binary_path, "sdkconfig"), os.path.join(self.binary_path, "..", "sdkconfig")]
  191. def get_sdkconfig(self):
  192. """
  193. reads sdkconfig and returns a dictionary with all configured variables
  194. :raise: AssertionError: if sdkconfig file does not exist in defined paths
  195. """
  196. d = {}
  197. sdkconfig_file = None
  198. for i in self._get_sdkconfig_paths():
  199. if os.path.exists(i):
  200. sdkconfig_file = i
  201. break
  202. assert sdkconfig_file is not None
  203. with open(sdkconfig_file) as f:
  204. for line in f:
  205. configs = line.split('=')
  206. if len(configs) == 2:
  207. d[configs[0]] = configs[1].rstrip()
  208. return d
  209. @abstractmethod
  210. def _try_get_binary_from_local_fs(self):
  211. pass
  212. def get_binary_path(self):
  213. path = self._try_get_binary_from_local_fs()
  214. if path:
  215. return path
  216. artifacts = self.artifact_cls(self.idf_path,
  217. self.case_group.get_artifact_index_file(),
  218. self.app_path, self.config_name, self.target)
  219. if isinstance(self, LoadableElfTestApp):
  220. assert self.app_files
  221. path = artifacts.download_artifact_files(self.app_files)
  222. else:
  223. path = artifacts.download_artifacts()
  224. if path:
  225. return os.path.join(self.idf_path, path)
  226. else:
  227. raise OSError("Failed to get binary for {}".format(self))
  228. def _get_elf_file_path(self):
  229. ret = ""
  230. file_names = os.listdir(self.binary_path)
  231. for fn in file_names:
  232. if os.path.splitext(fn)[1] == ".elf":
  233. ret = os.path.join(self.binary_path, fn)
  234. return ret
  235. def _parse_flash_download_config(self):
  236. """
  237. Parse flash download config from build metadata files
  238. Sets self.flash_files, self.flash_settings
  239. (Called from constructor)
  240. Returns (flash_files, flash_settings)
  241. """
  242. if self.IDF_FLASH_ARGS_FILE in os.listdir(self.binary_path):
  243. # CMake version using build metadata file
  244. path = os.path.join(self.binary_path, self.IDF_FLASH_ARGS_FILE)
  245. else:
  246. # GNU Make version uses download.config arguments file
  247. path = os.path.join(self.binary_path, self.IDF_DOWNLOAD_CONFIG_FILE)
  248. flash_files, flash_settings, app_name = parse_flash_settings(path)
  249. # The build metadata file does not currently have details, which files should be encrypted and which not.
  250. # Assume that all files should be encrypted if flash encryption is enabled in development mode.
  251. sdkconfig_dict = self.get_sdkconfig()
  252. flash_settings["encrypt"] = "CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT" in sdkconfig_dict
  253. # make file offsets into integers, make paths absolute
  254. flash_files = [(int(offs, 0), os.path.join(self.binary_path, file_path.strip())) for (offs, file_path) in flash_files]
  255. return flash_files, flash_settings
  256. def _parse_partition_table(self):
  257. """
  258. Parse partition table contents based on app binaries
  259. Returns partition_table data
  260. (Called from constructor)
  261. """
  262. partition_tool = os.path.join(self.idf_path,
  263. "components",
  264. "partition_table",
  265. "gen_esp32part.py")
  266. assert os.path.exists(partition_tool)
  267. errors = []
  268. # self.flash_files is sorted based on offset in order to have a consistent result with different versions of
  269. # Python
  270. for (_, path) in sorted(self.flash_files, key=lambda elem: elem[0]):
  271. if 'partition' in os.path.split(path)[1]:
  272. partition_file = os.path.join(self.binary_path, path)
  273. process = subprocess.Popen([sys.executable, partition_tool, partition_file],
  274. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  275. (raw_data, raw_error) = process.communicate()
  276. if isinstance(raw_error, bytes):
  277. raw_error = raw_error.decode()
  278. if 'Traceback' in raw_error:
  279. # Some exception occurred. It is possible that we've tried the wrong binary file.
  280. errors.append((path, raw_error))
  281. continue
  282. if isinstance(raw_data, bytes):
  283. raw_data = raw_data.decode()
  284. break
  285. else:
  286. traceback_msg = os.linesep.join(['{} {}:{}{}'.format(partition_tool,
  287. p,
  288. os.linesep,
  289. msg) for p, msg in errors])
  290. raise ValueError("No partition table found for IDF binary path: {}{}{}".format(self.binary_path,
  291. os.linesep,
  292. traceback_msg))
  293. partition_table = dict()
  294. for line in raw_data.splitlines():
  295. if line[0] != "#":
  296. try:
  297. _name, _type, _subtype, _offset, _size, _flags = line.split(",")
  298. if _size[-1] == "K":
  299. _size = int(_size[:-1]) * 1024
  300. elif _size[-1] == "M":
  301. _size = int(_size[:-1]) * 1024 * 1024
  302. else:
  303. _size = int(_size)
  304. _offset = int(_offset, 0)
  305. except ValueError:
  306. continue
  307. partition_table[_name] = {
  308. "type": _type,
  309. "subtype": _subtype,
  310. "offset": _offset,
  311. "size": _size,
  312. "flags": _flags
  313. }
  314. return partition_table
  315. def get_elf_sha256(self):
  316. if self._elf_file_sha256:
  317. return self._elf_file_sha256
  318. sha256 = hashlib.sha256()
  319. with open(self.elf_file, 'rb') as f:
  320. sha256.update(f.read())
  321. self._elf_file_sha256 = sha256.hexdigest()
  322. return self._elf_file_sha256
  323. class Example(IDFApp):
  324. def __init__(self, app_path, config_name='default', target='esp32', case_group=ExampleGroup, artifacts_cls=Artifacts):
  325. if not config_name:
  326. config_name = 'default'
  327. if not target:
  328. target = 'esp32'
  329. super(Example, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  330. def _get_sdkconfig_paths(self):
  331. """
  332. overrides the parent method to provide exact path of sdkconfig for example tests
  333. """
  334. return [os.path.join(self.binary_path, "..", "sdkconfig")]
  335. def _try_get_binary_from_local_fs(self):
  336. # build folder of example path
  337. path = os.path.join(self.idf_path, self.app_path, "build")
  338. if os.path.exists(path):
  339. return path
  340. # Search for CI build folders.
  341. # Path format: $IDF_PATH/build_examples/app_path_with_underscores/config/target
  342. # (see tools/ci/build_examples.sh)
  343. # For example: $IDF_PATH/build_examples/examples_get-started_blink/default/esp32
  344. app_path_underscored = self.app_path.replace(os.path.sep, "_")
  345. example_path = os.path.join(self.idf_path, self.case_group.LOCAL_BUILD_DIR)
  346. for dirpath in os.listdir(example_path):
  347. if os.path.basename(dirpath) == app_path_underscored:
  348. path = os.path.join(example_path, dirpath, self.config_name, self.target, "build")
  349. if os.path.exists(path):
  350. return path
  351. else:
  352. return None
  353. class UT(IDFApp):
  354. def __init__(self, app_path, config_name='default', target='esp32', case_group=UnitTestGroup, artifacts_cls=UnitTestArtifacts):
  355. if not config_name:
  356. config_name = 'default'
  357. if not target:
  358. target = 'esp32'
  359. super(UT, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  360. def _try_get_binary_from_local_fs(self):
  361. path = os.path.join(self.idf_path, self.app_path, "build")
  362. if os.path.exists(path):
  363. return path
  364. # first try to get from build folder of unit-test-app
  365. path = os.path.join(self.idf_path, "tools", "unit-test-app", "build")
  366. if os.path.exists(path):
  367. # found, use bin in build path
  368. return path
  369. # ``build_unit_test.sh`` will copy binary to output folder
  370. path = os.path.join(self.idf_path, "tools", "unit-test-app", "output", self.target, self.config_name)
  371. if os.path.exists(path):
  372. return path
  373. return None
  374. class TestApp(Example):
  375. def __init__(self, app_path, config_name='default', target='esp32', case_group=TestAppsGroup, artifacts_cls=Artifacts):
  376. super(TestApp, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  377. class LoadableElfTestApp(TestApp):
  378. def __init__(self, app_path, app_files, config_name='default', target='esp32', case_group=TestAppsGroup, artifacts_cls=Artifacts):
  379. # add arg `app_files` for loadable elf test_app.
  380. # Such examples only build elf files, so it doesn't generate flasher_args.json.
  381. # So we can't get app files from config file. Test case should pass it to application.
  382. super(IDFApp, self).__init__(app_path)
  383. self.app_path = app_path
  384. self.app_files = app_files
  385. self.config_name = config_name or 'default'
  386. self.target = target or 'esp32'
  387. self.idf_path = self.get_sdk_path()
  388. self.case_group = case_group
  389. self.artifact_cls = artifacts_cls
  390. self.binary_path = self.get_binary_path()
  391. self.elf_file = self._get_elf_file_path()
  392. assert os.path.exists(self.binary_path)
  393. class SSC(IDFApp):
  394. def get_binary_path(self):
  395. # TODO: to implement SSC get binary path
  396. return self.app_path
  397. class AT(IDFApp):
  398. def get_binary_path(self):
  399. # TODO: to implement AT get binary path
  400. return self.app_path