IDFApp.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  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, ComponentUTGroup
  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. # We use endswith here to avoid issue like:
  76. # examples_protocols_mqtt_ws but return a examples_protocols_mqtt_wss failure
  77. match_result = artifact_info["app_dir"].endswith(app_path)
  78. if config_name:
  79. match_result = match_result and config_name == artifact_info["config"]
  80. if target:
  81. match_result = match_result and target == artifact_info["target"]
  82. if match_result:
  83. ret = artifact_info
  84. break
  85. else:
  86. ret = None
  87. return ret
  88. def _get_app_base_path(self):
  89. if self.artifact_info:
  90. return os.path.join(self.artifact_info["work_dir"], self.artifact_info["build_dir"])
  91. else:
  92. return None
  93. def _get_flash_arg_file(self, base_path, job_id):
  94. if self.artifact_info["build_system"] == "cmake":
  95. flash_arg_file = os.path.join(base_path, "flasher_args.json")
  96. else:
  97. flash_arg_file = os.path.join(base_path, "download.config")
  98. self.gitlab_inst.download_artifact(job_id, [flash_arg_file], self.dest_root_path)
  99. return flash_arg_file
  100. def _download_binary_files(self, base_path, job_id, flash_arg_file):
  101. flash_files, flash_settings, app_name = parse_flash_settings(os.path.join(self.dest_root_path,
  102. flash_arg_file))
  103. artifact_files = [os.path.join(base_path, p[1]) for p in flash_files]
  104. artifact_files.append(os.path.join(base_path, app_name + ".elf"))
  105. self.gitlab_inst.download_artifact(job_id, artifact_files, self.dest_root_path)
  106. def _download_sdkconfig_file(self, base_path, job_id):
  107. self.gitlab_inst.download_artifact(job_id, [os.path.join(os.path.dirname(base_path), "sdkconfig")],
  108. self.dest_root_path)
  109. def download_artifacts(self):
  110. if not self.artifact_info:
  111. return None
  112. base_path = self._get_app_base_path()
  113. job_id = self.artifact_info["ci_job_id"]
  114. # 1. download flash args file
  115. flash_arg_file = self._get_flash_arg_file(base_path, job_id)
  116. # 2. download all binary files
  117. self._download_binary_files(base_path, job_id, flash_arg_file)
  118. # 3. download sdkconfig file
  119. self._download_sdkconfig_file(base_path, job_id)
  120. return base_path
  121. def download_artifact_files(self, file_names):
  122. if self.artifact_info:
  123. base_path = os.path.join(self.artifact_info["work_dir"], self.artifact_info["build_dir"])
  124. job_id = self.artifact_info["ci_job_id"]
  125. # download all binary files
  126. artifact_files = [os.path.join(base_path, fn) for fn in file_names]
  127. self.gitlab_inst.download_artifact(job_id, artifact_files, self.dest_root_path)
  128. # download sdkconfig file
  129. self.gitlab_inst.download_artifact(job_id, [os.path.join(os.path.dirname(base_path), "sdkconfig")],
  130. self.dest_root_path)
  131. else:
  132. base_path = None
  133. return base_path
  134. class UnitTestArtifacts(Artifacts):
  135. BUILDS_DIR_RE = re.compile(r'^builds/')
  136. def _get_app_base_path(self):
  137. if self.artifact_info:
  138. output_dir = self.BUILDS_DIR_RE.sub('output/', self.artifact_info["build_dir"])
  139. return os.path.join(self.artifact_info["app_dir"], output_dir)
  140. else:
  141. return None
  142. def _download_sdkconfig_file(self, base_path, job_id):
  143. self.gitlab_inst.download_artifact(job_id, [os.path.join(base_path, "sdkconfig")], self.dest_root_path)
  144. class IDFApp(App.BaseApp):
  145. """
  146. Implements common esp-idf application behavior.
  147. idf applications should inherent from this class and overwrite method get_binary_path.
  148. """
  149. IDF_DOWNLOAD_CONFIG_FILE = "download.config"
  150. IDF_FLASH_ARGS_FILE = "flasher_args.json"
  151. def __init__(self, app_path, config_name=None, target=None, case_group=IDFCaseGroup, artifact_cls=Artifacts):
  152. super(IDFApp, self).__init__(app_path)
  153. self.app_path = app_path
  154. self.config_name = config_name
  155. self.target = target
  156. self.idf_path = self.get_sdk_path()
  157. self.case_group = case_group
  158. self.artifact_cls = artifact_cls
  159. self.binary_path = self.get_binary_path()
  160. self.elf_file = self._get_elf_file_path()
  161. self._elf_file_sha256 = None
  162. assert os.path.exists(self.binary_path)
  163. if self.IDF_DOWNLOAD_CONFIG_FILE not in os.listdir(self.binary_path):
  164. if self.IDF_FLASH_ARGS_FILE not in os.listdir(self.binary_path):
  165. msg = ("Neither {} nor {} exists. "
  166. "Try to run 'make print_flash_cmd | tail -n 1 > {}/{}' "
  167. "or 'idf.py build' "
  168. "for resolving the issue."
  169. "").format(self.IDF_DOWNLOAD_CONFIG_FILE, self.IDF_FLASH_ARGS_FILE,
  170. self.binary_path, self.IDF_DOWNLOAD_CONFIG_FILE)
  171. raise AssertionError(msg)
  172. self.flash_files, self.flash_settings = self._parse_flash_download_config()
  173. self.partition_table = self._parse_partition_table()
  174. def __str__(self):
  175. parts = ['app<{}>'.format(self.app_path)]
  176. if self.config_name:
  177. parts.append('config<{}>'.format(self.config_name))
  178. if self.target:
  179. parts.append('target<{}>'.format(self.target))
  180. return ' '.join(parts)
  181. @classmethod
  182. def get_sdk_path(cls): # type: () -> str
  183. idf_path = os.getenv("IDF_PATH")
  184. assert idf_path
  185. assert os.path.exists(idf_path)
  186. return idf_path
  187. def _get_sdkconfig_paths(self):
  188. """
  189. returns list of possible paths where sdkconfig could be found
  190. Note: could be overwritten by a derived class to provide other locations or order
  191. """
  192. return [os.path.join(self.binary_path, "sdkconfig"), os.path.join(self.binary_path, "..", "sdkconfig")]
  193. def get_sdkconfig(self):
  194. """
  195. reads sdkconfig and returns a dictionary with all configured variables
  196. :raise: AssertionError: if sdkconfig file does not exist in defined paths
  197. """
  198. d = {}
  199. sdkconfig_file = None
  200. for i in self._get_sdkconfig_paths():
  201. if os.path.exists(i):
  202. sdkconfig_file = i
  203. break
  204. assert sdkconfig_file is not None
  205. with open(sdkconfig_file) as f:
  206. for line in f:
  207. configs = line.split('=')
  208. if len(configs) == 2:
  209. d[configs[0]] = configs[1].rstrip()
  210. return d
  211. @abstractmethod
  212. def _try_get_binary_from_local_fs(self):
  213. pass
  214. def get_binary_path(self):
  215. path = self._try_get_binary_from_local_fs()
  216. if path:
  217. return path
  218. artifacts = self.artifact_cls(self.idf_path,
  219. self.case_group.get_artifact_index_file(),
  220. self.app_path, self.config_name, self.target)
  221. if isinstance(self, LoadableElfTestApp):
  222. assert self.app_files
  223. path = artifacts.download_artifact_files(self.app_files)
  224. else:
  225. path = artifacts.download_artifacts()
  226. if path:
  227. return os.path.join(self.idf_path, path)
  228. else:
  229. raise OSError("Failed to get binary for {}".format(self))
  230. def _get_elf_file_path(self):
  231. ret = ""
  232. file_names = os.listdir(self.binary_path)
  233. for fn in file_names:
  234. if os.path.splitext(fn)[1] == ".elf":
  235. ret = os.path.join(self.binary_path, fn)
  236. return ret
  237. def _parse_flash_download_config(self):
  238. """
  239. Parse flash download config from build metadata files
  240. Sets self.flash_files, self.flash_settings
  241. (Called from constructor)
  242. Returns (flash_files, flash_settings)
  243. """
  244. if self.IDF_FLASH_ARGS_FILE in os.listdir(self.binary_path):
  245. # CMake version using build metadata file
  246. path = os.path.join(self.binary_path, self.IDF_FLASH_ARGS_FILE)
  247. else:
  248. # GNU Make version uses download.config arguments file
  249. path = os.path.join(self.binary_path, self.IDF_DOWNLOAD_CONFIG_FILE)
  250. flash_files, flash_settings, app_name = parse_flash_settings(path)
  251. # The build metadata file does not currently have details, which files should be encrypted and which not.
  252. # Assume that all files should be encrypted if flash encryption is enabled in development mode.
  253. sdkconfig_dict = self.get_sdkconfig()
  254. flash_settings["encrypt"] = "CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT" in sdkconfig_dict
  255. # make file offsets into integers, make paths absolute
  256. flash_files = [(int(offs, 0), os.path.join(self.binary_path, file_path.strip())) for (offs, file_path) in flash_files]
  257. return flash_files, flash_settings
  258. def _parse_partition_table(self):
  259. """
  260. Parse partition table contents based on app binaries
  261. Returns partition_table data
  262. (Called from constructor)
  263. """
  264. partition_tool = os.path.join(self.idf_path,
  265. "components",
  266. "partition_table",
  267. "gen_esp32part.py")
  268. assert os.path.exists(partition_tool)
  269. errors = []
  270. # self.flash_files is sorted based on offset in order to have a consistent result with different versions of
  271. # Python
  272. for (_, path) in sorted(self.flash_files, key=lambda elem: elem[0]):
  273. if 'partition' in os.path.split(path)[1]:
  274. partition_file = os.path.join(self.binary_path, path)
  275. process = subprocess.Popen([sys.executable, partition_tool, partition_file],
  276. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  277. (raw_data, raw_error) = process.communicate()
  278. if isinstance(raw_error, bytes):
  279. raw_error = raw_error.decode()
  280. if 'Traceback' in raw_error:
  281. # Some exception occurred. It is possible that we've tried the wrong binary file.
  282. errors.append((path, raw_error))
  283. continue
  284. if isinstance(raw_data, bytes):
  285. raw_data = raw_data.decode()
  286. break
  287. else:
  288. traceback_msg = os.linesep.join(['{} {}:{}{}'.format(partition_tool,
  289. p,
  290. os.linesep,
  291. msg) for p, msg in errors])
  292. raise ValueError("No partition table found for IDF binary path: {}{}{}".format(self.binary_path,
  293. os.linesep,
  294. traceback_msg))
  295. partition_table = dict()
  296. for line in raw_data.splitlines():
  297. if line[0] != "#":
  298. try:
  299. _name, _type, _subtype, _offset, _size, _flags = line.split(",")
  300. if _size[-1] == "K":
  301. _size = int(_size[:-1]) * 1024
  302. elif _size[-1] == "M":
  303. _size = int(_size[:-1]) * 1024 * 1024
  304. else:
  305. _size = int(_size)
  306. _offset = int(_offset, 0)
  307. except ValueError:
  308. continue
  309. partition_table[_name] = {
  310. "type": _type,
  311. "subtype": _subtype,
  312. "offset": _offset,
  313. "size": _size,
  314. "flags": _flags
  315. }
  316. return partition_table
  317. def get_elf_sha256(self):
  318. if self._elf_file_sha256:
  319. return self._elf_file_sha256
  320. sha256 = hashlib.sha256()
  321. with open(self.elf_file, 'rb') as f:
  322. sha256.update(f.read())
  323. self._elf_file_sha256 = sha256.hexdigest()
  324. return self._elf_file_sha256
  325. class Example(IDFApp):
  326. def __init__(self, app_path, config_name='default', target='esp32', case_group=ExampleGroup, artifacts_cls=Artifacts):
  327. if not config_name:
  328. config_name = 'default'
  329. if not target:
  330. target = 'esp32'
  331. super(Example, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  332. def _get_sdkconfig_paths(self):
  333. """
  334. overrides the parent method to provide exact path of sdkconfig for example tests
  335. """
  336. return [os.path.join(self.binary_path, "..", "sdkconfig")]
  337. def _try_get_binary_from_local_fs(self):
  338. # build folder of example path
  339. path = os.path.join(self.idf_path, self.app_path, "build")
  340. if os.path.exists(path):
  341. return path
  342. # Search for CI build folders.
  343. # Path format: $IDF_PATH/build_examples/app_path_with_underscores/config/target
  344. # (see tools/ci/build_examples.sh)
  345. # For example: $IDF_PATH/build_examples/examples_get-started_blink/default/esp32
  346. app_path_underscored = self.app_path.replace(os.path.sep, "_")
  347. example_path = os.path.join(self.idf_path, self.case_group.LOCAL_BUILD_DIR)
  348. for dirpath in os.listdir(example_path):
  349. if os.path.basename(dirpath) == app_path_underscored:
  350. path = os.path.join(example_path, dirpath, self.config_name, self.target, "build")
  351. if os.path.exists(path):
  352. return path
  353. else:
  354. return None
  355. class UT(IDFApp):
  356. def __init__(self, app_path, config_name='default', target='esp32', case_group=UnitTestGroup, artifacts_cls=UnitTestArtifacts):
  357. if not config_name:
  358. config_name = 'default'
  359. if not target:
  360. target = 'esp32'
  361. super(UT, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  362. def _try_get_binary_from_local_fs(self):
  363. path = os.path.join(self.idf_path, self.app_path, "build")
  364. if os.path.exists(path):
  365. return path
  366. # first try to get from build folder of unit-test-app
  367. path = os.path.join(self.idf_path, "tools", "unit-test-app", "build")
  368. if os.path.exists(path):
  369. # found, use bin in build path
  370. return path
  371. # ``build_unit_test.sh`` will copy binary to output folder
  372. path = os.path.join(self.idf_path, "tools", "unit-test-app", "output", self.target, self.config_name)
  373. if os.path.exists(path):
  374. return path
  375. return None
  376. class TestApp(Example):
  377. def __init__(self, app_path, config_name='default', target='esp32', case_group=TestAppsGroup, artifacts_cls=Artifacts):
  378. super(TestApp, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  379. class ComponentUTApp(TestApp):
  380. def __init__(self, app_path, config_name='default', target='esp32', case_group=ComponentUTGroup, artifacts_cls=Artifacts):
  381. super(ComponentUTApp, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  382. class LoadableElfTestApp(TestApp):
  383. def __init__(self, app_path, app_files, config_name='default', target='esp32', case_group=TestAppsGroup, artifacts_cls=Artifacts):
  384. # add arg `app_files` for loadable elf test_app.
  385. # Such examples only build elf files, so it doesn't generate flasher_args.json.
  386. # So we can't get app files from config file. Test case should pass it to application.
  387. super(IDFApp, self).__init__(app_path)
  388. self.app_path = app_path
  389. self.app_files = app_files
  390. self.config_name = config_name or 'default'
  391. self.target = target or 'esp32'
  392. self.idf_path = self.get_sdk_path()
  393. self.case_group = case_group
  394. self.artifact_cls = artifacts_cls
  395. self.binary_path = self.get_binary_path()
  396. self.elf_file = self._get_elf_file_path()
  397. assert os.path.exists(self.binary_path)
  398. class SSC(IDFApp):
  399. def get_binary_path(self):
  400. # TODO: to implement SSC get binary path
  401. return self.app_path
  402. class AT(IDFApp):
  403. def get_binary_path(self):
  404. # TODO: to implement AT get binary path
  405. return self.app_path