IDFApp.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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 ComponentUTGroup, ExampleGroup, IDFCaseGroup, TestAppsGroup, UnitTestGroup
  24. try:
  25. import gitlab_api
  26. except ImportError:
  27. gitlab_api = None
  28. def parse_encrypted_flag(args, offs, binary):
  29. # Find partition entries (e.g. the entries with an offset and a file)
  30. for _, entry in args.items():
  31. # If the current entry is a partition, we have to check whether it is
  32. # the one we are looking for or not
  33. try:
  34. if (entry['offset'], entry['file']) == (offs, binary):
  35. return entry['encrypted'] == 'true'
  36. except (TypeError, KeyError):
  37. # TypeError occurs if the entry is a list, which is possible in JSON
  38. # data structure.
  39. # KeyError occurs if the entry doesn't have "encrypted" field.
  40. continue
  41. # The entry was not found, return None. The caller will have to check
  42. # CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT macro
  43. return None
  44. def parse_flash_settings(path, default_encryption=False):
  45. file_name = os.path.basename(path)
  46. # For compatibility reasons, this list contains all the files to be
  47. # flashed
  48. flash_files = []
  49. # The following list only contains the files that need encryption
  50. encrypt_files = []
  51. if file_name == 'flasher_args.json':
  52. # CMake version using build metadata file
  53. with open(path, 'r') as f:
  54. args = json.load(f)
  55. for (offs, binary) in args['flash_files'].items():
  56. if offs:
  57. flash_files.append((offs, binary))
  58. encrypted = parse_encrypted_flag(args, offs, binary)
  59. # default_encryption should be taken into account if and only if
  60. # encrypted flag is not provided in the JSON file.
  61. if (encrypted is None and default_encryption) or encrypted:
  62. encrypt_files.append((offs, binary))
  63. flash_settings = args['flash_settings']
  64. app_name = os.path.splitext(args['app']['file'])[0]
  65. else:
  66. # GNU Make version uses download.config arguments file
  67. with open(path, 'r') as f:
  68. args = f.readlines()[-1].split(' ')
  69. flash_settings = {}
  70. for idx in range(0, len(args), 2): # process arguments in pairs
  71. if args[idx].startswith('--'):
  72. # strip the -- from the command line argument
  73. flash_settings[args[idx][2:]] = args[idx + 1]
  74. else:
  75. # offs, filename
  76. flash_files.append((args[idx], args[idx + 1]))
  77. # Parameter default_encryption tells us if the files need encryption
  78. if default_encryption:
  79. encrypt_files = flash_files
  80. # we can only guess app name in download.config.
  81. for p in flash_files:
  82. if not os.path.dirname(p[1]) and 'partition' not in p[1]:
  83. # app bin usually in the same dir with download.config and it's not partition table
  84. app_name = os.path.splitext(p[1])[0]
  85. break
  86. else:
  87. app_name = None
  88. return flash_files, encrypt_files, flash_settings, app_name
  89. class Artifacts(object):
  90. def __init__(self, dest_root_path, artifact_index_file, app_path, config_name, target):
  91. assert gitlab_api
  92. # at least one of app_path or config_name is not None. otherwise we can't match artifact
  93. assert app_path or config_name
  94. assert os.path.exists(artifact_index_file)
  95. self.gitlab_inst = gitlab_api.Gitlab(os.getenv('CI_PROJECT_ID'))
  96. self.dest_root_path = dest_root_path
  97. with open(artifact_index_file, 'r') as f:
  98. artifact_index = json.load(f)
  99. self.artifact_info = self._find_artifact(artifact_index, app_path, config_name, target)
  100. @staticmethod
  101. def _find_artifact(artifact_index, app_path, config_name, target):
  102. for artifact_info in artifact_index:
  103. match_result = True
  104. if app_path:
  105. # We use endswith here to avoid issue like:
  106. # examples_protocols_mqtt_ws but return a examples_protocols_mqtt_wss failure
  107. match_result = artifact_info['app_dir'].endswith(app_path)
  108. if config_name:
  109. match_result = match_result and config_name == artifact_info['config']
  110. if target:
  111. match_result = match_result and target == artifact_info['target']
  112. if match_result:
  113. ret = artifact_info
  114. break
  115. else:
  116. ret = None
  117. return ret
  118. def _get_app_base_path(self):
  119. if self.artifact_info:
  120. return os.path.join(self.artifact_info['work_dir'], self.artifact_info['build_dir'])
  121. else:
  122. return None
  123. def _get_flash_arg_file(self, base_path, job_id):
  124. if self.artifact_info['build_system'] == 'cmake':
  125. flash_arg_file = os.path.join(base_path, 'flasher_args.json')
  126. else:
  127. flash_arg_file = os.path.join(base_path, 'download.config')
  128. self.gitlab_inst.download_artifact(job_id, [flash_arg_file], self.dest_root_path)
  129. return flash_arg_file
  130. def _download_binary_files(self, base_path, job_id, flash_arg_file):
  131. # Let's ignore the second value returned (encrypt_files) as these
  132. # files also appear in the first list
  133. flash_files, _, _, app_name = parse_flash_settings(os.path.join(self.dest_root_path, flash_arg_file))
  134. artifact_files = [os.path.join(base_path, p[1]) for p in flash_files]
  135. artifact_files.append(os.path.join(base_path, app_name + '.elf'))
  136. self.gitlab_inst.download_artifact(job_id, artifact_files, self.dest_root_path)
  137. def _download_sdkconfig_file(self, base_path, job_id):
  138. self.gitlab_inst.download_artifact(job_id, [os.path.join(os.path.dirname(base_path), 'sdkconfig')],
  139. self.dest_root_path)
  140. def download_artifacts(self):
  141. if not self.artifact_info:
  142. return None
  143. base_path = self._get_app_base_path()
  144. job_id = self.artifact_info['ci_job_id']
  145. # 1. download flash args file
  146. flash_arg_file = self._get_flash_arg_file(base_path, job_id)
  147. # 2. download all binary files
  148. self._download_binary_files(base_path, job_id, flash_arg_file)
  149. # 3. download sdkconfig file
  150. self._download_sdkconfig_file(base_path, job_id)
  151. return base_path
  152. def download_artifact_files(self, file_names):
  153. if self.artifact_info:
  154. base_path = os.path.join(self.artifact_info['work_dir'], self.artifact_info['build_dir'])
  155. job_id = self.artifact_info['ci_job_id']
  156. # download all binary files
  157. artifact_files = [os.path.join(base_path, fn) for fn in file_names]
  158. self.gitlab_inst.download_artifact(job_id, artifact_files, self.dest_root_path)
  159. # download sdkconfig file
  160. self.gitlab_inst.download_artifact(job_id, [os.path.join(os.path.dirname(base_path), 'sdkconfig')],
  161. self.dest_root_path)
  162. else:
  163. base_path = None
  164. return base_path
  165. class UnitTestArtifacts(Artifacts):
  166. BUILDS_DIR_RE = re.compile(r'^builds/')
  167. def _get_app_base_path(self):
  168. if self.artifact_info:
  169. output_dir = self.BUILDS_DIR_RE.sub('output/', self.artifact_info['build_dir'])
  170. return os.path.join(self.artifact_info['app_dir'], output_dir)
  171. else:
  172. return None
  173. def _download_sdkconfig_file(self, base_path, job_id):
  174. self.gitlab_inst.download_artifact(job_id, [os.path.join(base_path, 'sdkconfig')], self.dest_root_path)
  175. class IDFApp(App.BaseApp):
  176. """
  177. Implements common esp-idf application behavior.
  178. idf applications should inherent from this class and overwrite method get_binary_path.
  179. """
  180. IDF_DOWNLOAD_CONFIG_FILE = 'download.config'
  181. IDF_FLASH_ARGS_FILE = 'flasher_args.json'
  182. def __init__(self, app_path, config_name=None, target=None, case_group=IDFCaseGroup, artifact_cls=Artifacts):
  183. super(IDFApp, self).__init__(app_path)
  184. self.app_path = app_path
  185. self.config_name = config_name
  186. self.target = target
  187. self.idf_path = self.get_sdk_path()
  188. self.case_group = case_group
  189. self.artifact_cls = artifact_cls
  190. self.binary_path = self.get_binary_path()
  191. self.elf_file = self._get_elf_file_path()
  192. self._elf_file_sha256 = None
  193. assert os.path.exists(self.binary_path)
  194. if self.IDF_DOWNLOAD_CONFIG_FILE not in os.listdir(self.binary_path):
  195. if self.IDF_FLASH_ARGS_FILE not in os.listdir(self.binary_path):
  196. msg = ('Neither {} nor {} exists. '
  197. "Try to run 'make print_flash_cmd | tail -n 1 > {}/{}' "
  198. "or 'idf.py build' "
  199. 'for resolving the issue.'
  200. '').format(self.IDF_DOWNLOAD_CONFIG_FILE, self.IDF_FLASH_ARGS_FILE,
  201. self.binary_path, self.IDF_DOWNLOAD_CONFIG_FILE)
  202. raise AssertionError(msg)
  203. # In order to keep backward compatibility, flash_files is unchanged.
  204. # However, we now have a new attribute encrypt_files.
  205. self.flash_files, self.encrypt_files, self.flash_settings = self._parse_flash_download_config()
  206. self.partition_table = self._parse_partition_table()
  207. def __str__(self):
  208. parts = ['app<{}>'.format(self.app_path)]
  209. if self.config_name:
  210. parts.append('config<{}>'.format(self.config_name))
  211. if self.target:
  212. parts.append('target<{}>'.format(self.target))
  213. return ' '.join(parts)
  214. @classmethod
  215. def get_sdk_path(cls): # type: () -> str
  216. idf_path = os.getenv('IDF_PATH')
  217. assert idf_path
  218. assert os.path.exists(idf_path)
  219. return idf_path
  220. def _get_sdkconfig_paths(self):
  221. """
  222. returns list of possible paths where sdkconfig could be found
  223. Note: could be overwritten by a derived class to provide other locations or order
  224. """
  225. return [os.path.join(self.binary_path, 'sdkconfig'), os.path.join(self.binary_path, '..', 'sdkconfig')]
  226. def get_sdkconfig(self):
  227. """
  228. reads sdkconfig and returns a dictionary with all configured variables
  229. :raise: AssertionError: if sdkconfig file does not exist in defined paths
  230. """
  231. d = {}
  232. sdkconfig_file = None
  233. for i in self._get_sdkconfig_paths():
  234. if os.path.exists(i):
  235. sdkconfig_file = i
  236. break
  237. assert sdkconfig_file is not None
  238. with open(sdkconfig_file) as f:
  239. for line in f:
  240. configs = line.split('=')
  241. if len(configs) == 2:
  242. d[configs[0]] = configs[1].rstrip()
  243. return d
  244. @abstractmethod
  245. def _try_get_binary_from_local_fs(self):
  246. pass
  247. def get_binary_path(self):
  248. path = self._try_get_binary_from_local_fs()
  249. if path:
  250. return path
  251. artifacts = self.artifact_cls(self.idf_path,
  252. self.case_group.get_artifact_index_file(),
  253. self.app_path, self.config_name, self.target)
  254. if isinstance(self, LoadableElfTestApp):
  255. assert self.app_files
  256. path = artifacts.download_artifact_files(self.app_files)
  257. else:
  258. path = artifacts.download_artifacts()
  259. if path:
  260. return os.path.join(self.idf_path, path)
  261. else:
  262. raise OSError('Failed to get binary for {}'.format(self))
  263. def _get_elf_file_path(self):
  264. ret = ''
  265. file_names = os.listdir(self.binary_path)
  266. for fn in file_names:
  267. if os.path.splitext(fn)[1] == '.elf':
  268. ret = os.path.join(self.binary_path, fn)
  269. return ret
  270. def _int_offs_abs_paths(self, files_list):
  271. return [(int(offs, 0),
  272. os.path.join(self.binary_path, file_path.strip()))
  273. for (offs, file_path) in files_list]
  274. def _parse_flash_download_config(self):
  275. """
  276. Parse flash download config from build metadata files
  277. Sets self.flash_files, self.flash_settings
  278. (Called from constructor)
  279. Returns (flash_files, encrypt_files, flash_settings)
  280. """
  281. if self.IDF_FLASH_ARGS_FILE in os.listdir(self.binary_path):
  282. # CMake version using build metadata file
  283. path = os.path.join(self.binary_path, self.IDF_FLASH_ARGS_FILE)
  284. else:
  285. # GNU Make version uses download.config arguments file
  286. path = os.path.join(self.binary_path, self.IDF_DOWNLOAD_CONFIG_FILE)
  287. # If the JSON doesn't find the encrypted flag for our files, provide
  288. # a default encrpytion flag: the macro
  289. # CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT
  290. sdkconfig_dict = self.get_sdkconfig()
  291. default_encryption = 'CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT' in sdkconfig_dict
  292. flash_files, encrypt_files, flash_settings, _ = parse_flash_settings(path, default_encryption)
  293. # Flash setting "encrypt" only and only if all the files to flash
  294. # must be encrypted. Else, this parameter should be False.
  295. # All files must be encrypted is both file lists are the same
  296. flash_settings['encrypt'] = sorted(flash_files) == sorted(encrypt_files)
  297. return self._int_offs_abs_paths(flash_files), self._int_offs_abs_paths(encrypt_files), flash_settings
  298. def _parse_partition_table(self):
  299. """
  300. Parse partition table contents based on app binaries
  301. Returns partition_table data
  302. (Called from constructor)
  303. """
  304. partition_tool = os.path.join(self.idf_path,
  305. 'components',
  306. 'partition_table',
  307. 'gen_esp32part.py')
  308. assert os.path.exists(partition_tool)
  309. errors = []
  310. # self.flash_files is sorted based on offset in order to have a consistent result with different versions of
  311. # Python
  312. for (_, path) in sorted(self.flash_files, key=lambda elem: elem[0]):
  313. if 'partition' in os.path.split(path)[1]:
  314. partition_file = os.path.join(self.binary_path, path)
  315. process = subprocess.Popen([sys.executable, partition_tool, partition_file],
  316. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  317. (raw_data, raw_error) = process.communicate()
  318. if isinstance(raw_error, bytes):
  319. raw_error = raw_error.decode()
  320. if 'Traceback' in raw_error:
  321. # Some exception occurred. It is possible that we've tried the wrong binary file.
  322. errors.append((path, raw_error))
  323. continue
  324. if isinstance(raw_data, bytes):
  325. raw_data = raw_data.decode()
  326. break
  327. else:
  328. traceback_msg = os.linesep.join(['{} {}:{}{}'.format(partition_tool,
  329. p,
  330. os.linesep,
  331. msg) for p, msg in errors])
  332. raise ValueError('No partition table found for IDF binary path: {}{}{}'.format(self.binary_path,
  333. os.linesep,
  334. traceback_msg))
  335. partition_table = dict()
  336. for line in raw_data.splitlines():
  337. if line[0] != '#':
  338. try:
  339. _name, _type, _subtype, _offset, _size, _flags = line.split(',')
  340. if _size[-1] == 'K':
  341. _size = int(_size[:-1]) * 1024
  342. elif _size[-1] == 'M':
  343. _size = int(_size[:-1]) * 1024 * 1024
  344. else:
  345. _size = int(_size)
  346. _offset = int(_offset, 0)
  347. except ValueError:
  348. continue
  349. partition_table[_name] = {
  350. 'type': _type,
  351. 'subtype': _subtype,
  352. 'offset': _offset,
  353. 'size': _size,
  354. 'flags': _flags
  355. }
  356. return partition_table
  357. def get_elf_sha256(self):
  358. if self._elf_file_sha256:
  359. return self._elf_file_sha256
  360. sha256 = hashlib.sha256()
  361. with open(self.elf_file, 'rb') as f:
  362. sha256.update(f.read())
  363. self._elf_file_sha256 = sha256.hexdigest()
  364. return self._elf_file_sha256
  365. class Example(IDFApp):
  366. def __init__(self, app_path, config_name='default', target='esp32', case_group=ExampleGroup, artifacts_cls=Artifacts):
  367. if not config_name:
  368. config_name = 'default'
  369. if not target:
  370. target = 'esp32'
  371. super(Example, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  372. def _get_sdkconfig_paths(self):
  373. """
  374. overrides the parent method to provide exact path of sdkconfig for example tests
  375. """
  376. return [os.path.join(self.binary_path, '..', 'sdkconfig')]
  377. def _try_get_binary_from_local_fs(self):
  378. # build folder of example path
  379. path = os.path.join(self.idf_path, self.app_path, 'build')
  380. if os.path.exists(path):
  381. return path
  382. # Search for CI build folders.
  383. # Path format: $IDF_PATH/build_examples/app_path_with_underscores/config/target
  384. # (see tools/ci/build_examples.sh)
  385. # For example: $IDF_PATH/build_examples/examples_get-started_blink/default/esp32
  386. app_path_underscored = self.app_path.replace(os.path.sep, '_')
  387. example_path = os.path.join(self.idf_path, self.case_group.LOCAL_BUILD_DIR)
  388. for dirpath in os.listdir(example_path):
  389. if os.path.basename(dirpath) == app_path_underscored:
  390. path = os.path.join(example_path, dirpath, self.config_name, self.target, 'build')
  391. if os.path.exists(path):
  392. return path
  393. else:
  394. return None
  395. class UT(IDFApp):
  396. def __init__(self, app_path, config_name='default', target='esp32', case_group=UnitTestGroup, artifacts_cls=UnitTestArtifacts):
  397. if not config_name:
  398. config_name = 'default'
  399. if not target:
  400. target = 'esp32'
  401. super(UT, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  402. def _try_get_binary_from_local_fs(self):
  403. path = os.path.join(self.idf_path, self.app_path, 'build')
  404. if os.path.exists(path):
  405. return path
  406. # first try to get from build folder of unit-test-app
  407. path = os.path.join(self.idf_path, 'tools', 'unit-test-app', 'build')
  408. if os.path.exists(path):
  409. # found, use bin in build path
  410. return path
  411. # ``build_unit_test.sh`` will copy binary to output folder
  412. path = os.path.join(self.idf_path, 'tools', 'unit-test-app', 'output', self.target, self.config_name)
  413. if os.path.exists(path):
  414. return path
  415. return None
  416. class TestApp(Example):
  417. def __init__(self, app_path, config_name='default', target='esp32', case_group=TestAppsGroup, artifacts_cls=Artifacts):
  418. super(TestApp, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  419. class ComponentUTApp(TestApp):
  420. def __init__(self, app_path, config_name='default', target='esp32', case_group=ComponentUTGroup, artifacts_cls=Artifacts):
  421. super(ComponentUTApp, self).__init__(app_path, config_name, target, case_group, artifacts_cls)
  422. class LoadableElfTestApp(TestApp):
  423. def __init__(self, app_path, app_files, config_name='default', target='esp32', case_group=TestAppsGroup, artifacts_cls=Artifacts):
  424. # add arg `app_files` for loadable elf test_app.
  425. # Such examples only build elf files, so it doesn't generate flasher_args.json.
  426. # So we can't get app files from config file. Test case should pass it to application.
  427. super(IDFApp, self).__init__(app_path)
  428. self.app_path = app_path
  429. self.app_files = app_files
  430. self.config_name = config_name or 'default'
  431. self.target = target or 'esp32'
  432. self.idf_path = self.get_sdk_path()
  433. self.case_group = case_group
  434. self.artifact_cls = artifacts_cls
  435. self.binary_path = self.get_binary_path()
  436. self.elf_file = self._get_elf_file_path()
  437. assert os.path.exists(self.binary_path)
  438. class SSC(IDFApp):
  439. def get_binary_path(self):
  440. # TODO: to implement SSC get binary path
  441. return self.app_path
  442. class AT(IDFApp):
  443. def get_binary_path(self):
  444. # TODO: to implement AT get binary path
  445. return self.app_path