CIAssignUnitTest.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. """
  2. Command line tool to assign unit tests to CI test jobs.
  3. """
  4. import re
  5. import argparse
  6. import yaml
  7. try:
  8. from yaml import CLoader as Loader
  9. except ImportError:
  10. from yaml import Loader as Loader
  11. from tiny_test_fw.Utility import CIAssignTest
  12. class Group(CIAssignTest.Group):
  13. SORT_KEYS = ["test environment", "tags"]
  14. MAX_CASE = 50
  15. ATTR_CONVERT_TABLE = {
  16. "execution_time": "execution time"
  17. }
  18. # when IDF support multiple chips, SDK will be moved into tags, we can remove it
  19. CI_JOB_MATCH_KEYS = ["test environment", "SDK"]
  20. def __init__(self, case):
  21. super(Group, self).__init__(case)
  22. for tag in self._get_case_attr(case, "tags"):
  23. self.ci_job_match_keys.add(tag)
  24. @staticmethod
  25. def _get_case_attr(case, attr):
  26. if attr in Group.ATTR_CONVERT_TABLE:
  27. attr = Group.ATTR_CONVERT_TABLE[attr]
  28. return case[attr]
  29. def add_extra_case(self, case):
  30. """ If current group contains all tags required by case, then add succeed """
  31. added = False
  32. if self.accept_new_case():
  33. for key in self.filters:
  34. if self._get_case_attr(case, key) != self.filters[key]:
  35. if key == "tags":
  36. if self._get_case_attr(case, key).issubset(self.filters[key]):
  37. continue
  38. break
  39. else:
  40. self.case_list.append(case)
  41. added = True
  42. return added
  43. def _create_extra_data(self, test_cases, test_function):
  44. """
  45. For unit test case, we need to copy some attributes of test cases into config file.
  46. So unit test function knows how to run the case.
  47. """
  48. case_data = []
  49. for case in test_cases:
  50. one_case_data = {
  51. "config": self._get_case_attr(case, "config"),
  52. "name": self._get_case_attr(case, "summary"),
  53. "reset": self._get_case_attr(case, "reset"),
  54. "timeout": self._get_case_attr(case, "timeout"),
  55. }
  56. if test_function in ["run_multiple_devices_cases", "run_multiple_stage_cases"]:
  57. try:
  58. one_case_data["child case num"] = self._get_case_attr(case, "child case num")
  59. except KeyError as e:
  60. print("multiple devices/stages cases must contains at least two test functions")
  61. print("case name: {}".format(one_case_data["name"]))
  62. raise e
  63. case_data.append(one_case_data)
  64. return case_data
  65. def _divide_case_by_test_function(self):
  66. """
  67. divide cases of current test group by test function they need to use
  68. :return: dict of list of cases for each test functions
  69. """
  70. case_by_test_function = {
  71. "run_multiple_devices_cases": [],
  72. "run_multiple_stage_cases": [],
  73. "run_unit_test_cases": [],
  74. }
  75. for case in self.case_list:
  76. if case["multi_device"] == "Yes":
  77. case_by_test_function["run_multiple_devices_cases"].append(case)
  78. elif case["multi_stage"] == "Yes":
  79. case_by_test_function["run_multiple_stage_cases"].append(case)
  80. else:
  81. case_by_test_function["run_unit_test_cases"].append(case)
  82. return case_by_test_function
  83. def output(self):
  84. """
  85. output data for job configs
  86. :return: {"Filter": case filter, "CaseConfig": list of case configs for cases in this group}
  87. """
  88. case_by_test_function = self._divide_case_by_test_function()
  89. output_data = {
  90. # we don't need filter for test function, as UT uses a few test functions for all cases
  91. "CaseConfig": [
  92. {
  93. "name": test_function,
  94. "extra_data": self._create_extra_data(test_cases, test_function),
  95. } for test_function, test_cases in case_by_test_function.items() if test_cases
  96. ],
  97. }
  98. return output_data
  99. class UnitTestAssignTest(CIAssignTest.AssignTest):
  100. CI_TEST_JOB_PATTERN = re.compile(r"^UT_.+")
  101. def __init__(self, test_case_path, ci_config_file):
  102. CIAssignTest.AssignTest.__init__(self, test_case_path, ci_config_file, case_group=Group)
  103. def _search_cases(self, test_case_path, case_filter=None):
  104. """
  105. For unit test case, we don't search for test functions.
  106. The unit test cases is stored in a yaml file which is created in job build-idf-test.
  107. """
  108. try:
  109. with open(test_case_path, "r") as f:
  110. raw_data = yaml.load(f, Loader=Loader)
  111. test_cases = raw_data["test cases"]
  112. for case in test_cases:
  113. case["tags"] = set(case["tags"])
  114. except IOError:
  115. print("Test case path is invalid. Should only happen when use @bot to skip unit test.")
  116. test_cases = []
  117. # filter keys are lower case. Do map lower case keys with original keys.
  118. try:
  119. key_mapping = {x.lower(): x for x in test_cases[0].keys()}
  120. except IndexError:
  121. key_mapping = dict()
  122. if case_filter:
  123. for key in case_filter:
  124. filtered_cases = []
  125. for case in test_cases:
  126. try:
  127. mapped_key = key_mapping[key]
  128. # bot converts string to lower case
  129. if isinstance(case[mapped_key], str):
  130. _value = case[mapped_key].lower()
  131. else:
  132. _value = case[mapped_key]
  133. if _value in case_filter[key]:
  134. filtered_cases.append(case)
  135. except KeyError:
  136. # case don't have this key, regard as filter success
  137. filtered_cases.append(case)
  138. test_cases = filtered_cases
  139. # sort cases with configs and test functions
  140. # in later stage cases with similar attributes are more likely to be assigned to the same job
  141. # it will reduce the count of flash DUT operations
  142. test_cases.sort(key=lambda x: x["config"] + x["multi_stage"] + x["multi_device"])
  143. return test_cases
  144. if __name__ == '__main__':
  145. parser = argparse.ArgumentParser()
  146. parser.add_argument("test_case",
  147. help="test case folder or file")
  148. parser.add_argument("ci_config_file",
  149. help="gitlab ci config file")
  150. parser.add_argument("output_path",
  151. help="output path of config files")
  152. args = parser.parse_args()
  153. assign_test = UnitTestAssignTest(args.test_case, args.ci_config_file)
  154. assign_test.assign_cases()
  155. assign_test.output_configs(args.output_path)