text: """
  Command line tool to assign unit tests to CI test jobs.
  """
  
  import re
  import os
  import sys
  import argparse
  
  import yaml
  
  try:
      from Utility import CIAssignTest
  except ImportError:
      test_fw_path = os.getenv("TEST_FW_PATH")
      if test_fw_path:
          sys.path.insert(0, test_fw_path)
      from Utility import CIAssignTest
  
  
  class Group(CIAssignTest.Group):
      SORT_KEYS = ["config", "SDK", "test environment", "multi_device", "multi_stage", "tags"]
      MAX_CASE = 30
      ATTR_CONVERT_TABLE = {
          "execution_time": "execution time"
      }
      # when IDF support multiple chips, SDK will be moved into tags, we can remove it
      CI_JOB_MATCH_KEYS = ["test environment", "SDK"]
  
      def __init__(self, case):
          super(Group, self).__init__(case)
          for tag in self._get_case_attr(case, "tags"):
              self.ci_job_match_keys.add(tag)
  
      @staticmethod
      def _get_case_attr(case, attr):
          if attr in Group.ATTR_CONVERT_TABLE:
              attr = Group.ATTR_CONVERT_TABLE[attr]
          return case[attr]
  
      def _create_extra_data(self, test_function):
          """
          For unit test case, we need to copy some attributes of test cases into config file.
          So unit test function knows how to run the case.
          """
          case_data = []
          for case in self.case_list:
              one_case_data = {
                  "config": self._get_case_attr(case, "config"),
                  "name": self._get_case_attr(case, "summary"),
                  "reset": self._get_case_attr(case, "reset"),
                  "timeout": self._get_case_attr(case, "timeout"),
              }
  
              if test_function in ["run_multiple_devices_cases", "run_multiple_stage_cases"]:
                  try:
                      one_case_data["child case num"] = self._get_case_attr(case, "child case num")
                  except KeyError as e:
                      print("multiple devices/stages cases must contains at least two test functions")
                      print("case name: {}".format(one_case_data["name"]))
                      raise e
  
              case_data.append(one_case_data)
          return case_data
  
      def _map_test_function(self):
          """
          determine which test function to use according to current test case
  
          :return: test function name to use
          """
          if self.filters["multi_device"] == "Yes":
              test_function = "run_multiple_devices_cases"
          elif self.filters["multi_stage"] == "Yes":
              test_function = "run_multiple_stage_cases"
          else:
              test_function = "run_unit_test_cases"
          return test_function
  
      def output(self):
          """
          output data for job configs
  
          :return: {"Filter": case filter, "CaseConfig": list of case configs for cases in this group}
          """
          test_function = self._map_test_function()
          output_data = {
              # we don't need filter for test function, as UT uses a few test functions for all cases
              "CaseConfig": [
                  {
                      "name": test_function,
                      "extra_data": self._create_extra_data(test_function),
                  }
              ]
          }
          return output_data
  
  
  class UnitTestAssignTest(CIAssignTest.AssignTest):
      CI_TEST_JOB_PATTERN = re.compile(r"^UT_.+")
  
      def __init__(self, test_case_path, ci_config_file):
          CIAssignTest.AssignTest.__init__(self, test_case_path, ci_config_file, case_group=Group)
  
      def _search_cases(self, test_case_path, case_filter=None):
          """
          For unit test case, we don't search for test functions.
          The unit test cases is stored in a yaml file which is created in job build-idf-test.
          """
  
          try:
              with open(test_case_path, "r") as f:
                  raw_data = yaml.load(f)
              test_cases = raw_data["test cases"]
          except IOError:
              print("Test case path is invalid. Should only happen when use @bot to skip unit test.")
              test_cases = []
          # filter keys are lower case. Do map lower case keys with original keys.
          try:
              key_mapping = {x.lower(): x for x in test_cases[0].keys()}
          except IndexError:
              key_mapping = dict()
          if case_filter:
              for key in case_filter:
                  filtered_cases = []
                  for case in test_cases:
                      try:
                          mapped_key = key_mapping[key]
                          # bot converts string to lower case
                          if isinstance(case[mapped_key], str):
                              _value = case[mapped_key].lower()
                          else:
                              _value = case[mapped_key]
                          if _value in case_filter[key]:
                              filtered_cases.append(case)
                      except KeyError:
                          # case don't have this key, regard as filter success
                          filtered_cases.append(case)
                  test_cases = filtered_cases
          return test_cases
  
  
  if __name__ == '__main__':
      parser = argparse.ArgumentParser()
      parser.add_argument("test_case",
                          help="test case folder or file")
      parser.add_argument("ci_config_file",
                          help="gitlab ci config file")
      parser.add_argument("output_path",
                          help="output path of config files")
      args = parser.parse_args()
  
      assign_test = UnitTestAssignTest(args.test_case, args.ci_config_file)
      assign_test.assign_cases()
      assign_test.output_configs(args.output_path)
  