UnitTestParser.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. from __future__ import print_function
  2. import yaml
  3. import os
  4. import re
  5. import shutil
  6. import subprocess
  7. from copy import deepcopy
  8. import CreateSectionTable
  9. TEST_CASE_PATTERN = {
  10. "initial condition": "UTINIT1",
  11. "SDK": "ESP32_IDF",
  12. "level": "Unit",
  13. "execution time": 0,
  14. "auto test": "Yes",
  15. "category": "Function",
  16. "test point 1": "basic function",
  17. "version": "v1 (2016-12-06)",
  18. "test environment": "UT_T1_1",
  19. "reset": "",
  20. "expected result": "1. set succeed",
  21. "cmd set": "test_unit_test_case",
  22. "Test App": "UT",
  23. }
  24. class Parser(object):
  25. """ parse unit test cases from build files and create files for test bench """
  26. TAG_PATTERN = re.compile("([^=]+)(=)?(.+)?")
  27. DESCRIPTION_PATTERN = re.compile("\[([^]\[]+)\]")
  28. CONFIG_PATTERN = re.compile(r"{([^}]+)}")
  29. TEST_GROUPS_PATTERN = re.compile(r"TEST_GROUPS=(.*)$")
  30. # file path (relative to idf path)
  31. TAG_DEF_FILE = os.path.join("tools", "unit-test-app", "tools", "TagDefinition.yml")
  32. MODULE_DEF_FILE = os.path.join("tools", "unit-test-app", "tools", "ModuleDefinition.yml")
  33. CONFIG_DEPENDENCY_FILE = os.path.join("tools", "unit-test-app", "tools", "ConfigDependency.yml")
  34. MODULE_ARTIFACT_FILE = os.path.join("components", "idf_test", "ModuleDefinition.yml")
  35. TEST_CASE_FILE = os.path.join("components", "idf_test", "unit_test", "TestCaseAll.yml")
  36. UT_BIN_FOLDER = os.path.join("tools", "unit-test-app", "output")
  37. UT_CONFIG_FOLDER = os.path.join("tools", "unit-test-app", "configs")
  38. ELF_FILE = "unit-test-app.elf"
  39. SDKCONFIG_FILE = "sdkconfig"
  40. def __init__(self, idf_path=os.getenv("IDF_PATH")):
  41. self.test_env_tags = {}
  42. self.unit_jobs = {}
  43. self.file_name_cache = {}
  44. self.idf_path = idf_path
  45. self.tag_def = yaml.load(open(os.path.join(idf_path, self.TAG_DEF_FILE), "r"))
  46. self.module_map = yaml.load(open(os.path.join(idf_path, self.MODULE_DEF_FILE), "r"))
  47. self.config_dependencies = yaml.load(open(os.path.join(idf_path, self.CONFIG_DEPENDENCY_FILE), "r"))
  48. # used to check if duplicated test case names
  49. self.test_case_names = set()
  50. self.parsing_errors = []
  51. def parse_test_cases_for_one_config(self, configs_folder, config_output_folder, config_name):
  52. """
  53. parse test cases from elf and save test cases need to be executed to unit test folder
  54. :param configs_folder: folder where per-config sdkconfig framents are located (i.e. tools/unit-test-app/configs)
  55. :param config_output_folder: build folder of this config
  56. :param config_name: built unit test config name
  57. """
  58. test_groups = self.get_test_groups(os.path.join(configs_folder, config_name))
  59. elf_file = os.path.join(config_output_folder, self.ELF_FILE)
  60. subprocess.check_output('xtensa-esp32-elf-objdump -t {} | grep test_desc > case_address.tmp'.format(elf_file),
  61. shell=True)
  62. subprocess.check_output('xtensa-esp32-elf-objdump -s {} > section_table.tmp'.format(elf_file), shell=True)
  63. table = CreateSectionTable.SectionTable("section_table.tmp")
  64. tags = self.parse_tags(os.path.join(config_output_folder, self.SDKCONFIG_FILE))
  65. test_cases = []
  66. with open("case_address.tmp", "rb") as f:
  67. for line in f:
  68. # process symbol table like: "3ffb4310 l O .dram0.data 00000018 test_desc_33$5010"
  69. line = line.split()
  70. test_addr = int(line[0], 16)
  71. section = line[3]
  72. name_addr = table.get_unsigned_int(section, test_addr, 4)
  73. desc_addr = table.get_unsigned_int(section, test_addr + 4, 4)
  74. file_name_addr = table.get_unsigned_int(section, test_addr + 12, 4)
  75. function_count = table.get_unsigned_int(section, test_addr+20, 4)
  76. name = table.get_string("any", name_addr)
  77. desc = table.get_string("any", desc_addr)
  78. file_name = table.get_string("any", file_name_addr)
  79. tc = self.parse_one_test_case(name, desc, file_name, config_name, tags)
  80. # check if duplicated case names
  81. # we need to use it to select case,
  82. # if duplicated IDs, Unity could select incorrect case to run
  83. # and we need to check all cases no matter if it's going te be executed by CI
  84. # also add app_name here, we allow same case for different apps
  85. if (tc["summary"] + config_name) in self.test_case_names:
  86. self.parsing_errors.append("duplicated test case ID: " + tc["summary"])
  87. else:
  88. self.test_case_names.add(tc["summary"] + config_name)
  89. test_group_included = True
  90. if test_groups is not None and tc["group"] not in test_groups:
  91. test_group_included = False
  92. if tc["CI ready"] == "Yes" and test_group_included:
  93. # update test env list and the cases of same env list
  94. if tc["test environment"] in self.test_env_tags:
  95. self.test_env_tags[tc["test environment"]].append(tc["ID"])
  96. else:
  97. self.test_env_tags.update({tc["test environment"]: [tc["ID"]]})
  98. if function_count > 1:
  99. tc.update({"child case num": function_count})
  100. # only add cases need to be executed
  101. test_cases.append(tc)
  102. os.remove("section_table.tmp")
  103. os.remove("case_address.tmp")
  104. return test_cases
  105. def parse_case_properities(self, tags_raw):
  106. """
  107. parse test case tags (properities) with the following rules:
  108. * first tag is always group of test cases, it's mandatory
  109. * the rest tags should be [type=value].
  110. * if the type have default value, then [type] equal to [type=default_value].
  111. * if the type don't don't exist, then equal to [type=omitted_value]
  112. default_value and omitted_value are defined in TagDefinition.yml
  113. :param tags_raw: raw tag string
  114. :return: tag dict
  115. """
  116. tags = self.DESCRIPTION_PATTERN.findall(tags_raw)
  117. assert len(tags) > 0
  118. p = dict([(k, self.tag_def[k]["omitted"]) for k in self.tag_def])
  119. p["module"] = tags[0]
  120. # Use the original value of the first tag as test group name
  121. p["group"] = p["module"]
  122. if p["module"] not in self.module_map:
  123. p["module"] = "misc"
  124. # parsing rest tags, [type=value], =value is optional
  125. for tag in tags[1:]:
  126. match = self.TAG_PATTERN.search(tag)
  127. assert match is not None
  128. tag_type = match.group(1)
  129. tag_value = match.group(3)
  130. if match.group(2) == "=" and tag_value is None:
  131. # [tag_type=] means tag_value is empty string
  132. tag_value = ""
  133. if tag_type in p:
  134. if tag_value is None:
  135. p[tag_type] = self.tag_def[tag_type]["default"]
  136. else:
  137. p[tag_type] = tag_value
  138. else:
  139. # ignore not defined tag type
  140. pass
  141. return p
  142. @staticmethod
  143. def parse_tags_internal(sdkconfig, config_dependencies, config_pattern):
  144. required_tags = []
  145. def compare_config(config):
  146. return config in sdkconfig
  147. def process_condition(condition):
  148. matches = config_pattern.findall(condition)
  149. if matches:
  150. for config in matches:
  151. compare_result = compare_config(config)
  152. # replace all configs in condition with True or False according to compare result
  153. condition = re.sub(config_pattern, str(compare_result), condition, count=1)
  154. # Now the condition is a python condition, we can use eval to compute its value
  155. ret = eval(condition)
  156. else:
  157. # didn't use complex condition. only defined one condition for the tag
  158. ret = compare_config(condition)
  159. return ret
  160. for tag in config_dependencies:
  161. if process_condition(config_dependencies[tag]):
  162. required_tags.append(tag)
  163. return required_tags
  164. def parse_tags(self, sdkconfig_file):
  165. """
  166. Some test configs could requires different DUTs.
  167. For example, if CONFIG_SPIRAM_SUPPORT is enabled, we need WROVER-Kit to run test.
  168. This method will get tags for runners according to ConfigDependency.yml(maps tags to sdkconfig).
  169. We support to the following syntax::
  170. # define the config which requires the tag
  171. 'tag_a': 'config_a="value_a"'
  172. # define the condition for the tag
  173. 'tag_b': '{config A} and (not {config B} or (not {config C} and {config D}))'
  174. :param sdkconfig_file: sdk config file of the unit test config
  175. :return: required tags for runners
  176. """
  177. with open(sdkconfig_file, "r") as f:
  178. configs_raw_data = f.read()
  179. configs = configs_raw_data.splitlines(False)
  180. return self.parse_tags_internal(configs, self.config_dependencies, self.CONFIG_PATTERN)
  181. def get_test_groups(self, config_file):
  182. """
  183. If the config file includes TEST_GROUPS variable, return its value as a list of strings.
  184. :param config_file file under configs/ directory for given configuration
  185. :return: list of test groups, or None if TEST_GROUPS wasn't set
  186. """
  187. with open(config_file, "r") as f:
  188. for line in f:
  189. match = self.TEST_GROUPS_PATTERN.match(line)
  190. if match is not None:
  191. return match.group(1).split(' ')
  192. return None
  193. def parse_one_test_case(self, name, description, file_name, config_name, tags):
  194. """
  195. parse one test case
  196. :param name: test case name (summary)
  197. :param description: test case description (tag string)
  198. :param file_name: the file defines this test case
  199. :param config_name: built unit test app name
  200. :param tags: tags to select runners
  201. :return: parsed test case
  202. """
  203. prop = self.parse_case_properities(description)
  204. test_case = deepcopy(TEST_CASE_PATTERN)
  205. test_case.update({"config": config_name,
  206. "module": self.module_map[prop["module"]]['module'],
  207. "group": prop["group"],
  208. "CI ready": "No" if prop["ignore"] == "Yes" else "Yes",
  209. "ID": name,
  210. "test point 2": prop["module"],
  211. "steps": name,
  212. "test environment": prop["test_env"],
  213. "reset": prop["reset"],
  214. "sub module": self.module_map[prop["module"]]['sub module'],
  215. "summary": name,
  216. "multi_device": prop["multi_device"],
  217. "multi_stage": prop["multi_stage"],
  218. "timeout": int(prop["timeout"]),
  219. "tags": tags})
  220. return test_case
  221. def dump_test_cases(self, test_cases):
  222. """
  223. dump parsed test cases to YAML file for test bench input
  224. :param test_cases: parsed test cases
  225. """
  226. with open(os.path.join(self.idf_path, self.TEST_CASE_FILE), "w+") as f:
  227. yaml.dump({"test cases": test_cases}, f, allow_unicode=True, default_flow_style=False)
  228. def copy_module_def_file(self):
  229. """ copy module def file to artifact path """
  230. src = os.path.join(self.idf_path, self.MODULE_DEF_FILE)
  231. dst = os.path.join(self.idf_path, self.MODULE_ARTIFACT_FILE)
  232. shutil.copy(src, dst)
  233. def parse_test_cases(self):
  234. """ parse test cases from multiple built unit test apps """
  235. test_cases = []
  236. output_folder = os.path.join(self.idf_path, self.UT_BIN_FOLDER)
  237. configs_folder = os.path.join(self.idf_path, self.UT_CONFIG_FOLDER)
  238. test_configs = os.listdir(output_folder)
  239. for config in test_configs:
  240. config_output_folder = os.path.join(output_folder, config)
  241. if os.path.exists(config_output_folder):
  242. test_cases.extend(self.parse_test_cases_for_one_config(configs_folder, config_output_folder, config))
  243. test_cases.sort(key=lambda x: x["config"] + x["summary"])
  244. self.dump_test_cases(test_cases)
  245. def test_parser():
  246. parser = Parser()
  247. # test parsing tags
  248. # parsing module only and module in module list
  249. prop = parser.parse_case_properities("[esp32]")
  250. assert prop["module"] == "esp32"
  251. # module not in module list
  252. prop = parser.parse_case_properities("[not_in_list]")
  253. assert prop["module"] == "misc"
  254. # parsing a default tag, a tag with assigned value
  255. prop = parser.parse_case_properities("[esp32][ignore][test_env=ABCD][not_support1][not_support2=ABCD]")
  256. assert prop["ignore"] == "Yes" and prop["test_env"] == "ABCD" \
  257. and "not_support1" not in prop and "not_supported2" not in prop
  258. # parsing omitted value
  259. prop = parser.parse_case_properities("[esp32]")
  260. assert prop["ignore"] == "No" and prop["test_env"] == "UT_T1_1"
  261. # parsing with incorrect format
  262. try:
  263. parser.parse_case_properities("abcd")
  264. assert False
  265. except AssertionError:
  266. pass
  267. # skip invalid data parse, [type=] assigns empty string to type
  268. prop = parser.parse_case_properities("[esp32]abdc aaaa [ignore=]")
  269. assert prop["module"] == "esp32" and prop["ignore"] == ""
  270. # skip mis-paired []
  271. prop = parser.parse_case_properities("[esp32][[ignore=b]][]][test_env=AAA]]")
  272. assert prop["module"] == "esp32" and prop["ignore"] == "b" and prop["test_env"] == "AAA"
  273. config_dependency = {
  274. 'a': '123',
  275. 'b': '456',
  276. 'c': 'not {123}',
  277. 'd': '{123} and not {456}',
  278. 'e': '{123} and not {789}',
  279. 'f': '({123} and {456}) or ({123} and {789})'
  280. }
  281. sdkconfig = ["123", "789"]
  282. tags = parser.parse_tags_internal(sdkconfig, config_dependency, parser.CONFIG_PATTERN)
  283. assert sorted(tags) == ['a', 'd', 'f'] # sorted is required for older Python3, e.g. 3.4.8
  284. def main():
  285. test_parser()
  286. idf_path = os.getenv("IDF_PATH")
  287. parser = Parser(idf_path)
  288. parser.parse_test_cases()
  289. parser.copy_module_def_file()
  290. if len(parser.parsing_errors) > 0:
  291. for error in parser.parsing_errors:
  292. print(error)
  293. exit(-1)
  294. if __name__ == '__main__':
  295. main()