]> granicus.if.org Git - esp-idf/commitdiff
component/bt: implement classic Bluetooth profiles A2DP(sink) and AVRCP(controller)
authorwangmengyang <wangmengyang@espressif.com>
Mon, 10 Apr 2017 08:12:21 +0000 (16:12 +0800)
committerwangmengyang <wangmengyang@espressif.com>
Mon, 10 Apr 2017 08:12:21 +0000 (16:12 +0800)
12 files changed:
1  2 
.gitlab-ci.yml
components/bt/Kconfig
components/bt/bluedroid/osi/include/thread.h
components/esp32/cpu_start.c
components/esp32/include/esp_err.h
components/idf_test/unit_test/InitialConditionAll.yml
examples/bluetooth/a2dp_sink/main/demo_main.c
tools/unit-test-app/UnitTestParser.py
tools/unit-test-app/tools/CreateSectionTable.py
tools/unit-test-app/tools/ModuleDefinition.yml
tools/unit-test-app/tools/TagDefinition.yml
tools/unit-test-app/tools/UnitTestParser.py

diff --cc .gitlab-ci.yml
index ef0635139e594c79caab0922553c342f2c43a933,ef0635139e594c79caab0922553c342f2c43a933..14c870267640d61e69b6374746d61ae81565b9de
@@@ -105,9 -105,9 +105,8 @@@ build_esp_idf_tests
  
    script:
      - cd tools/unit-test-app
--    - git checkout ${CI_BUILD_REF_NAME} || echo "Using default branch..."
      - make TESTS_ALL=1
--    - python UnitTestParser.py
++    - python tools/UnitTestParser.py
  
  build_examples:
    <<: *build_template
@@@ -143,7 -143,7 +142,6 @@@ build_docs
        - docs/_build/html
      expire_in: 1 mos
  
--
  test_nvs_on_host:
    stage: test
    image: $CI_DOCKER_REGISTRY/esp32-ci-env
@@@ -294,6 -294,6 +292,21 @@@ check_doc_links
        - docs/_build/linkcheck
      expire_in: 1 mos
  
++check_commit_msg:
++  stage: deploy
++  image: $CI_DOCKER_REGISTRY/esp32-ci-env
++  tags:
++    - build
++  except:
++    - master
++    - /^release\/v/
++    - /^v\d+\.\d+(\.\d+)?($|-)/
++  before_script:
++    - echo "skip update submodule"
++  script:
++    - git checkout ${CI_BUILD_REF_NAME}
++    # commit start with "WIP: " need to be squashed before merge
++    - 'git log --pretty=%s master..${CI_BUILD_REF_NAME} | grep "^WIP: " || exit 0 && exit 1'
  
  # AUTO GENERATED PART START, DO NOT MODIFY CONTENT BELOW
  # template for test jobs
      LOG_PATH: "$CI_PROJECT_DIR/$CI_BUILD_REF"
      APP_NAME: "ut"
      TEST_CASE_FILE_PATH: "$CI_PROJECT_DIR/components/idf_test/unit_test"
--    MODULE_UPDATE_FILE: "$CI_PROJECT_DIR/tools/unit-test-app/ModuleDefinition.yml"
++    MODULE_UPDATE_FILE: "$CI_PROJECT_DIR/tools/unit-test-app/tools/ModuleDefinition.yml"
    
    dependencies:
      - build_esp_idf_tests
index d9021e9601bfef59b0959df49c27b695fca26e7b,5b2cc3e6aa39e97647187c1c7d5313bee50cd7f3..eaa3d1625fb0f4d215d6be8fd0e192e38ffff377
@@@ -35,9 -30,47 +35,47 @@@ config BT_DRAM_RELEAS
      default n
      help
          This option should only be used when BLE only.
 -        Open this option will release about 30K DRAM from Classic BT.
 +        Enabling this option will release about 30K DRAM from Classic BT.
          The released DRAM will be used as system heap memory.
  
 -    depends on BT_ENABLED
+ #disable now for app cpu due to a known issue
+ config BTDM_CONTROLLER_RUN_APP_CPU
+     bool "Run controller on APP CPU"
+     depends on BT_ENABLED && !FREERTOS_UNICORE && 0
+     default n
+     help
+         Run controller on APP CPU.
+ config BTDM_CONTROLLER_RUN_CPU
+     int
+     depends on BT_ENABLED
+     default 1 if BTDM_CONTROLLER_RUN_APP_CPU
+     default 0
+ menuconfig BT_HCI_UART
+     bool "HCI use UART as IO"
++    depends on BT_ENABLED && !BLUEDROID_ENABLED
+     default n
+     help
+         Default HCI use VHCI, if this option choose, HCI will use UART(0/1/2) as IO.
+         Besides, it can set uart number and uart baudrate.
+ config BT_HCI_UART_NO
+     int "UART Number for HCI"
+     depends on BT_HCI_UART
+     range 1 2
+     default 1
+     help
+         Uart number for HCI.
+ config BT_HCI_UART_BAUDRATE
+     int "UART Baudrate for HCI"
+     depends on BT_HCI_UART
+     range 115200 921600
+     default 921600
+     help
+         UART Baudrate for HCI. Please use standard baudrate.
  # Memory reserved at start of DRAM for Bluetooth stack
  config BT_RESERVE_DRAM
      hex
index 495a178034c5b3f95ba8f90a82c6df64234e919e,9c06a49f265560fcc874f28e14d001441864a765..e0ad356ab9691bdf2d7585de98ff3608cad50025
@@@ -206,9 -227,9 +227,7 @@@ void start_cpu0_default(void
  #if CONFIG_TASK_WDT
      esp_task_wdt_init();
  #endif
--#if !CONFIG_FREERTOS_UNICORE
      esp_crosscore_int_init();
--#endif
      esp_ipc_init();
      spi_flash_init();
      /* init default OS-aware flash access critical section */
index c7beafd37563b08c7291ede1dd85d0bac1e0a9d7,c7beafd37563b08c7291ede1dd85d0bac1e0a9d7..2990c8938e3fb3bc26f92a750fa4771e980758f6
@@@ -61,7 -61,7 +61,10 @@@ void _esp_error_check_failed(esp_err_t 
   * Disabled if assertions are disabled.
   */
  #ifdef NDEBUG
--#define ESP_ERROR_CHECK(x) do { (x); } while (0)
++#define ESP_ERROR_CHECK(x) do {                                         \
++        esp_err_t rc = (x);                                             \
++        (void) sizeof(rc);                                              \
++    } while(0);
  #else
  #define ESP_ERROR_CHECK(x) do {                                         \
          esp_err_t rc = (x);                                             \
index 3821894552aafc6daa03919a2f66a9da2afce838,3821894552aafc6daa03919a2f66a9da2afce838..48045c286d620fb6489869e3e87a462841e77733
@@@ -2935,12 -2935,12 +2935,18 @@@ initial condition
    test script: InitCondBase
  - check cmd set:
    - ''
++  - - FREBOOT UT1
++    - ['']
++  - - DELAY 3
++    - ['']
    - - UT UT1 -
      - [R UT1 C Tests C Failures C Ignored]
    force restore cmd set:
    - ''
    - - FREBOOT UT1
      - ['']
++  - - DELAY 3
++    - ['']
    - - UT UT1 -
      - [R UT1 C Tests C Failures C Ignored]
    initial condition detail: At UT menu page
    - ''
    - - FREBOOT UT1
      - ['']
++  - - DELAY 3
++    - ['']
    - - UT UT1 -
      - [R UT1 C Tests C Failures C Ignored]
    restore post cmd set:
index 8a90e6b5a3d24aef5b73b6e8cafa0f37f3c85d49,0000000000000000000000000000000000000000..3511a3007a7a9812123821579381f805a0a47d03
mode 100755,000000..100755
--- /dev/null
@@@ -1,25 -1,0 +1,38 @@@
-     esp_bt_controller_init();
 +#include <stdio.h>
 +#include <stdlib.h>
 +#include <unistd.h>
 +#include <string.h>
 +#include "bt.h"
 +#include "freertos/FreeRTOS.h"
 +#include "freertos/task.h"
 +
 +#include "nvs_flash.h"
 +#include "esp_system.h"
++#include "esp_log.h"
++
++#define A2DP_SINK_TAG    "A2DP_SINK"
 +
 +extern void bte_main_boot_entry(void *);
 +extern void bt_app_task_start_up(void);
 +extern void bt_app_core_start(void);
 +
 +void app_main()
 +{
++    esp_err_t ret;
 +    nvs_flash_init();
-     if (esp_bt_controller_enable(ESP_BT_MODE_BTDM) != ESP_OK) {
 +
++    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
++    ret = esp_bt_controller_init(&bt_cfg);
++    if (ret) {
++        ESP_LOGE(A2DP_SINK_TAG, "%s initialize controller failed\n", __func__);
++        return;
++    }
++
++    ret = esp_bt_controller_enable(ESP_BT_MODE_BTDM);
++    if (ret) {
++        ESP_LOGE(A2DP_SINK_TAG, "%s enable controller failed\n", __func__);
 +        return;
 +    }
++    
 +    bt_app_task_start_up();
 +}
diff --cc tools/unit-test-app/UnitTestParser.py
index d3fa1efb0259ff86a517a81db7baa3ff070b21e0,d3fa1efb0259ff86a517a81db7baa3ff070b21e0..0000000000000000000000000000000000000000
deleted file mode 100644,100644
+++ /dev/null
@@@ -1,170 -1,170 +1,0 @@@
--import yaml
--import os
--import os.path
--import re
--import sys
--import shutil
--
--
--MODULE_MAP = yaml.load(open("ModuleDefinition.yml", "r"))
--
--TEST_CASE_PATTERN = {
--    "initial condition": "UTINIT1",
--    "SDK": "ESP32_IDF",
--    "level": "Unit",
--    "execution time": 0,
--    "Test App": "UT",
--    "auto test": "Yes",
--    "category": "Function",
--    "test point 1": "basic function",
--    "version": "v1 (2016-12-06)",
--    "test environment": "UT_T1_1",
--    "expected result": "1. set succeed"
--}
--
--CONFIG_FILE_PATTERN = {
--    "Config": {"execute count": 1, "execute order": "in order"},
--    "DUT": [],
--    "Filter": [{"Add": {"ID": []}}]
--}
--
--test_cases = list()
--test_ids = {}
--test_ids_by_job = {}
--unit_jobs = {}
--
--os.chdir(os.path.join("..", ".."))
--IDF_PATH = os.getcwd()
--
--
--class Parser(object):
--    @classmethod
--    def parse_test_folders(cls):
--        test_folder_paths = list()
--        os.chdir(os.path.join(IDF_PATH, "components"))
--        component_dirs = [d for d in os.listdir(".") if os.path.isdir(d)]
--        for dir in component_dirs:
--            os.chdir(dir)
--            if "test" in os.listdir("."):
--                test_folder_paths.append(os.path.join(os.getcwd(), "test"))
--            os.chdir("..")
--        Parser.parse_test_files(test_folder_paths)
--
--    @classmethod
--    def parse_test_files(cls, test_folder_paths):
--        for path in test_folder_paths:
--            os.chdir(path)
--            for file_path in os.listdir("."):
--                if file_path[-2:] == ".c":
--                    Parser.read_test_file(os.path.join(os.getcwd(), file_path), len(test_cases)+1)
--        os.chdir(os.path.join("..", ".."))
--        Parser.dump_test_cases(test_cases)
--
--    @classmethod
--    def read_test_file(cls, test_file_path, file_index):
--        test_index = 0
--        with open(test_file_path, "r") as file:
--            for line in file:
--                if re.match("TEST_CASE", line):
--                    test_index += 1
--                    tags = re.split(r"[\[\]\"]", line)
--                    Parser.parse_test_cases(file_index, test_index, tags)
--
--
--    @classmethod
--    def parse_test_cases(cls, file_index, test_index, tags):
--        ci_ready = "Yes"
--        test_env = "UT_T1_1"
--        for tag in tags:
--            if tag == "ignore":
--                ci_ready = "No"
--            if re.match("test_env=", tag):
--                test_env = tag[9:]
--        module_name = tags[4]
--        try:
--            MODULE_MAP[module_name]
--        except KeyError:
--            module_name = "misc"
--        id = "UT_%s_%s_%03d%02d" % (MODULE_MAP[module_name]['module abbr'],
--                                    MODULE_MAP[module_name]['sub module abbr'],
--                                    file_index, test_index)
--        test_case = dict(TEST_CASE_PATTERN)
--        test_case.update({"module": MODULE_MAP[module_name]['module'], 
--                          "CI ready": ci_ready,
--                          "cmd set": ["IDFUnitTest/UnitTest", [tags[1]]],
--                          "ID": id,
--                          "test point 2": module_name,
--                          "steps": tags[1],
--                          "comment": tags[1],
--                          "test environment": test_env,
--                          "sub module": MODULE_MAP[module_name]['sub module'],
--                          "summary": tags[1]})
--        if test_case["CI ready"] == "Yes":
--            if test_ids.has_key(test_env):
--                test_ids[test_env].append(id)
--            else:
--                test_ids.update({test_env: [id]})
--        test_cases.append(test_case)
--
--    @classmethod
--    def dump_test_cases(cls, test_cases):
--        os.chdir(os.path.join(IDF_PATH, "components", "idf_test", "unit_test"))
--        with open ("TestCaseAll.yml", "wb+") as f:
--            yaml.dump({"test cases": test_cases}, f, allow_unicode=True, default_flow_style=False)
--
--    @classmethod
--    def dump_ci_config(cls):
--        Parser.split_test_cases()
--        os.chdir(os.path.join(IDF_PATH, "components", "idf_test", "unit_test"))
--        if not os.path.exists("CIConfigs"):
--            os.makedirs("CIConfigs")
--        os.chdir("CIConfigs")
--        for unit_job in unit_jobs:
--            job = dict(CONFIG_FILE_PATTERN)
--            job.update({"DUT": ["UT1"]})
--            job.update({"Filter": [{"Add": {"ID": test_ids_by_job[unit_job]}}]})
--            with open (unit_job + ".yml", "wb+") as f:
--                yaml.dump(job, f, allow_unicode=True, default_flow_style=False)
--
--    @classmethod
--    def split_test_cases(cls):
--        for job in unit_jobs:
--            test_ids_by_job.update({job: list()})
--        for test_env in test_ids:
--            available_jobs = list()
--            for job in unit_jobs:
--                if test_env in unit_jobs[job]:
--                    available_jobs.append(job)
--            for idx, job in enumerate(available_jobs):
--                test_ids_by_job[job] += (test_ids[test_env][idx*len(test_ids[test_env])/len(available_jobs):(idx+1)*len(test_ids[test_env])/len(available_jobs)])
--
--    @classmethod
--    def parse_gitlab_ci(cls):
--        os.chdir(IDF_PATH)
--        with open(".gitlab-ci.yml", "rb") as f:
--            gitlab_ci = yaml.load(f)
--            keys = gitlab_ci.keys()
--            for key in keys:
--                if re.match("UT_", key):
--                    test_env = gitlab_ci[key]["tags"]
--                    unit_job = key
--                    key = {}
--                    key.update({unit_job: test_env})
--                    unit_jobs.update(key)
--
--    @classmethod
--    def copy_module_def_file(cls):
--        src = os.path.join(IDF_PATH, "tools", "unit-test-app", "ModuleDefinition.yml")
--        dst = os.path.join(IDF_PATH, "components", "idf_test", "unit_test")
--        shutil.copy(src, dst)
--
--
--def main():
--    Parser.parse_test_folders()
--    Parser.parse_gitlab_ci()
--    Parser.dump_ci_config()
--    Parser.copy_module_def_file()
--
--
--if __name__ == '__main__':
--    main()
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..a9379cf049e3397ee5b6cb163f28f9eb281c010f
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,163 @@@
++# This file is used to process section data generated by `objdump -s`
++import re
++
++
++class Section(object):
++    """
++    One Section of section table. contains info about section name, address and raw data
++    """
++    SECTION_START_PATTERN = re.compile("Contents of section (.+?):")
++    DATA_PATTERN = re.compile("([0-9a-f]{4,8})")
++
++    def __init__(self, name, start_address, data):
++        self.name = name
++        self.start_address = start_address
++        self.data = data
++
++    def __contains__(self, item):
++        """ check if the section name and address match this section """
++        if (item["section"] == self.name or item["section"] == "any") \
++                and (self.start_address <= item["address"] < (self.start_address + len(self.data))):
++            return True
++        else:
++            return False
++
++    def __getitem__(self, item):
++        """ 
++        process slice. 
++        convert absolute address to relative address in current section and return slice result
++        """
++        if isinstance(item, int):
++            return self.data[item - self.start_address]
++        elif isinstance(item, slice):
++            start = item.start if item.start is None else item.start - self.start_address
++            stop = item.stop if item.stop is None else item.stop - self.start_address
++            return self.data[start:stop]
++        return self.data[item]
++
++    def __str__(self):
++        return "%s [%08x - %08x]" % (self.name, self.start_address, self.start_address + len(self.data))
++
++    __repr__ = __str__
++
++    @classmethod
++    def parse_raw_data(cls, raw_data):
++        """
++        process raw data generated by `objdump -s`, create section and return un-processed lines
++        :param raw_data: lines of raw data generated by `objdump -s`
++        :return: one section, un-processed lines
++        """
++        name = ""
++        data = ""
++        start_address = 0
++        # first find start line
++        for i, line in enumerate(raw_data):
++            if "Contents of section " in line:  # do strcmp first to speed up
++                match = cls.SECTION_START_PATTERN.search(line)
++                if match is not None:
++                    name = match.group(1)
++                    raw_data = raw_data[i + 1:]
++                    break
++        else:
++            # do some error handling
++            raw_data = [""]  # add a dummy first data line
++
++        def process_data_line(line_to_process):
++            # first remove the ascii part
++            hex_part = line_to_process.split("  ")[0]
++            # process rest part
++            data_list = cls.DATA_PATTERN.findall(hex_part)
++            try:
++                _address = int(data_list[0], base=16)
++            except IndexError:
++                _address = -1
++
++            def hex_to_str(hex_data):
++                if len(hex_data) % 2 == 1:
++                    hex_data = "0" + hex_data  # append zero at the beginning
++                _length = len(hex_data)
++                return "".join([chr(int(hex_data[_i:_i + 2], base=16))
++                                for _i in range(0, _length, 2)])
++
++            return _address, "".join([hex_to_str(x) for x in data_list[1:]])
++
++        # handle first line:
++        address, _data = process_data_line(raw_data[0])
++        if address != -1:
++            start_address = address
++            data += _data
++            raw_data = raw_data[1:]
++            for i, line in enumerate(raw_data):
++                address, _data = process_data_line(line)
++                if address == -1:
++                    raw_data = raw_data[i:]
++                    break
++                else:
++                    data += _data
++        else:
++            # do error handling
++            raw_data = []
++
++        section = cls(name, start_address, data) if start_address != -1 else None
++        unprocessed_data = None if len(raw_data) == 0 else raw_data
++        return section, unprocessed_data
++
++
++class SectionTable(object):
++    """ elf section table """
++
++    def __init__(self, file_name):
++        with open(file_name, "rb") as f:
++            raw_data = f.readlines()
++        self.table = []
++        while raw_data:
++            section, raw_data = Section.parse_raw_data(raw_data)
++            self.table.append(section)
++
++    def get_unsigned_int(self, section, address, size=4, endian="LE"):
++        """
++        get unsigned int from section table
++        :param section: section name; use "any" will only match with address
++        :param address: start address
++        :param size: size in bytes
++        :param endian: LE or BE
++        :return: int or None
++        """
++        if address % 4 != 0 or size % 4 != 0:
++            print("warning: try to access without 4 bytes aligned")
++        key = {"address": address, "section": section}
++        for section in self.table:
++            if key in section:
++                tmp = section[address:address+size]
++                value = 0
++                for i in range(size):
++                    if endian == "LE":
++                        value += ord(tmp[i]) << (i*8)
++                    elif endian == "BE":
++                        value += ord(tmp[i]) << ((size - i - 1) * 8)
++                    else:
++                        print("only support LE or BE for parameter endian")
++                        assert False
++                break
++        else:
++            value = None
++        return value
++
++    def get_string(self, section, address):
++        """
++        get string ('\0' terminated) from section table
++        :param section: section name; use "any" will only match with address
++        :param address: start address
++        :return: string or None
++        """
++        value = None
++        key = {"address": address, "section": section}
++        for section in self.table:
++            if key in section:
++                value = section[address:]
++                for i, c in enumerate(value):
++                    if c == '\0':
++                        value = value[:i]
++                        break
++                break
++        return value
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..3b952eaed657ddef634aae07e012019ae80b288a
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,8 @@@
++ignore:
++  # if the type exist but no value assigned
++  default: "Yes"
++  # if the type is not exist in tag list
++  omitted: "No"
++test_env:
++  default: "UT_T1_1"
++  omitted: "UT_T1_1"
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..0f151e9d92e48b2f40666c250041147d1ff6991b
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,262 @@@
++import yaml
++import os
++import re
++import shutil
++import subprocess
++
++from copy import deepcopy
++import CreateSectionTable
++
++
++TEST_CASE_PATTERN = {
++    "initial condition": "UTINIT1",
++    "SDK": "ESP32_IDF",
++    "level": "Unit",
++    "execution time": 0,
++    "Test App": "UT",
++    "auto test": "Yes",
++    "category": "Function",
++    "test point 1": "basic function",
++    "version": "v1 (2016-12-06)",
++    "test environment": "UT_T1_1",
++    "expected result": "1. set succeed"
++}
++
++CONFIG_FILE_PATTERN = {
++    "Config": {"execute count": 1, "execute order": "in order"},
++    "DUT": [],
++    "Filter": [{"Add": {"ID": []}}]
++}
++
++
++class Parser(object):
++    """ parse unit test cases from build files and create files for test bench """
++
++    TAG_PATTERN = re.compile("([^=]+)(=)?(.+)?")
++    DESCRIPTION_PATTERN = re.compile("\[([^]\[]+)\]")
++
++    def __init__(self, idf_path=os.getenv("IDF_PATH")):
++        self.test_env_tags = {}
++        self.unit_jobs = {}
++        self.file_name_cache = {}
++        self.idf_path = idf_path
++        self.tag_def = yaml.load(open(os.path.join(idf_path, "tools", "unit-test-app", "tools",
++                                                   "TagDefinition.yml"), "r"))
++        self.module_map = yaml.load(open(os.path.join(idf_path, "tools", "unit-test-app", "tools",
++                                                      "ModuleDefinition.yml"), "r"))
++
++    def parse_test_cases_from_elf(self, elf_file):
++        """
++        parse test cases from elf and save test cases to unit test folder
++        :param elf_file: elf file path
++        """
++        subprocess.check_output('xtensa-esp32-elf-objdump -t {} | grep \ test_desc > case_address.tmp'.format(elf_file),
++                                shell=True)
++        subprocess.check_output('xtensa-esp32-elf-objdump -s {} > section_table.tmp'.format(elf_file), shell=True)
++
++        table = CreateSectionTable.SectionTable("section_table.tmp")
++        test_cases = []
++        with open("case_address.tmp", "r") as f:
++            for line in f:
++                # process symbol table like: "3ffb4310 l     O .dram0.data    00000018 test_desc_33$5010"
++                line = line.split()
++                test_addr = int(line[0], 16)
++                section = line[3]
++
++                name_addr = table.get_unsigned_int(section, test_addr, 4)
++                desc_addr = table.get_unsigned_int(section, test_addr + 4, 4)
++                file_name_addr = table.get_unsigned_int(section, test_addr + 12, 4)
++                name = table.get_string("any", name_addr)
++                desc = table.get_string("any", desc_addr)
++                file_name = table.get_string("any", file_name_addr)
++
++                tc = self.parse_one_test_case(name, desc, file_name)
++                if tc["CI ready"] == "Yes":
++                    # update test env list and the cases of same env list
++                    if tc["test environment"] in self.test_env_tags:
++                        self.test_env_tags[tc["test environment"]].append(tc["ID"])
++                    else:
++                        self.test_env_tags.update({tc["test environment"]: [tc]})
++                test_cases.append(tc)
++
++        os.remove("section_table.tmp")
++        os.remove("case_address.tmp")
++
++        self.dump_test_cases(test_cases)
++
++    def parse_case_properities(self, tags_raw):
++        """
++        parse test case tags (properities) with the following rules:
++            * first tag is always group of test cases, it's mandatory
++            * the rest tags should be [type=value]. 
++                * if the type have default value, then [type] equal to [type=default_value].
++                * if the type don't don't exist, then equal to [type=omitted_value]
++            default_value and omitted_value are defined in TagDefinition.yml
++        :param tags_raw: raw tag string
++        :return: tag dict
++        """
++        tags = self.DESCRIPTION_PATTERN.findall(tags_raw)
++        assert len(tags) > 0
++        p = dict([(k, self.tag_def[k]["omitted"]) for k in self.tag_def])
++        p["module"] = tags[0]
++
++        if p["module"] not in self.module_map:
++            p["module"] = "misc"
++
++        # parsing rest tags, [type=value], =value is optional
++        for tag in tags[1:]:
++            match = self.TAG_PATTERN.search(tag)
++            assert match is not None
++            tag_type = match.group(1)
++            tag_value = match.group(3)
++            if match.group(2) == "=" and tag_value is None:
++                # [tag_type=] means tag_value is empty string
++                tag_value = ""
++            if tag_type in p:
++                if tag_value is None:
++                    p[tag_type] = self.tag_def[tag_type]["default"]
++                else:
++                    p[tag_type] = tag_value
++            else:
++                # ignore not defined tag type
++                pass
++        return p
++
++    def parse_one_test_case(self, name, description, file_name):
++        """
++        parse one test case
++        :param name: test case name (summary)
++        :param description: test case description (tag string)
++        :param file_name: the file defines this test case
++        :return: parsed test case
++        """
++        prop = self.parse_case_properities(description)
++
++        if file_name in self.file_name_cache:
++            self.file_name_cache[file_name] += 1
++        else:
++            self.file_name_cache[file_name] = 1
++
++        tc_id = "UT_%s_%s_%03d%02d" % (self.module_map[prop["module"]]['module abbr'],
++                                       self.module_map[prop["module"]]['sub module abbr'],
++                                       hash(file_name) % 1000,
++                                       self.file_name_cache[file_name])
++        test_case = deepcopy(TEST_CASE_PATTERN)
++        test_case.update({"module": self.module_map[prop["module"]]['module'],
++                          "CI ready": "No" if prop["ignore"] == "Yes" else "Yes",
++                          "cmd set": ["IDFUnitTest/UnitTest", [name]],
++                          "ID": tc_id,
++                          "test point 2": prop["module"],
++                          "steps": name,
++                          "test environment": prop["test_env"],
++                          "sub module": self.module_map[prop["module"]]['sub module'],
++                          "summary": name})
++        return test_case
++
++    def dump_test_cases(self, test_cases):
++        """
++        dump parsed test cases to YAML file for test bench input
++        :param test_cases: parsed test cases
++        """
++        with open(os.path.join(self.idf_path, "components", "idf_test", "unit_test", "TestCaseAll.yml"), "wb+") as f:
++            yaml.dump({"test cases": test_cases}, f, allow_unicode=True, default_flow_style=False)
++
++    def dump_ci_config(self):
++        """ assign test cases and dump to config file to test bench """
++        test_cases_by_jobs = self.assign_test_cases()
++
++        ci_config_folder = os.path.join(self.idf_path, "components", "idf_test", "unit_test", "CIConfigs")
++
++        if not os.path.exists(ci_config_folder):
++            os.makedirs(os.path.join(ci_config_folder, "CIConfigs"))
++
++        for unit_job in self.unit_jobs:
++            job = deepcopy(CONFIG_FILE_PATTERN)
++            job.update({"DUT": ["UT1"]})
++            job.update({"Filter": [{"Add": {"ID": test_cases_by_jobs[unit_job]}}]})
++
++            with open(os.path.join(ci_config_folder, unit_job + ".yml"), "wb+") as f:
++                yaml.dump(job, f, allow_unicode=True, default_flow_style=False)
++
++    def assign_test_cases(self):
++        """ assign test cases to jobs  """
++        test_cases_by_jobs = {}
++
++        for job in self.unit_jobs:
++            test_cases_by_jobs.update({job: list()})
++        for test_env in self.test_env_tags:
++            available_jobs = list()
++            for job in self.unit_jobs:
++                if test_env in self.unit_jobs[job]:
++                    available_jobs.append(job)
++            for idx, job in enumerate(available_jobs):
++                test_cases_by_jobs[job] += (self.test_env_tags[test_env]
++                                            [idx*len(self.test_env_tags[test_env])/len(available_jobs):
++                                            (idx+1)*len(self.test_env_tags[test_env])/len(available_jobs)])
++        return test_cases_by_jobs
++
++    def parse_gitlab_ci(self):
++        """ parse gitlab ci config file to get pre-defined unit test jobs """
++        with open(os.path.join(self.idf_path, ".gitlab-ci.yml"), "r") as f:
++            gitlab_ci = yaml.load(f)
++            keys = gitlab_ci.keys()
++            for key in keys:
++                if re.match("UT_", key):
++                    test_env = gitlab_ci[key]["tags"]
++                    unit_job = key
++                    key = {}
++                    key.update({unit_job: test_env})
++                    self.unit_jobs.update(key)
++
++    def copy_module_def_file(self):
++        """ copy module def file to artifact path """
++        src = os.path.join(self.idf_path, "tools", "unit-test-app", "tools", "ModuleDefinition.yml")
++        dst = os.path.join(self.idf_path, "components", "idf_test", "unit_test")
++        shutil.copy(src, dst)
++
++
++def test_parser():
++    parser = Parser()
++    # test parsing tags
++    # parsing module only and module in module list
++    prop = parser.parse_case_properities("[esp32]")
++    assert prop["module"] == "esp32"
++    # module not in module list
++    prop = parser.parse_case_properities("[not_in_list]")
++    assert prop["module"] == "misc"
++    # parsing a default tag, a tag with assigned value
++    prop = parser.parse_case_properities("[esp32][ignore][test_env=ABCD][not_support1][not_support2=ABCD]")
++    assert prop["ignore"] == "Yes" and prop["test_env"] == "ABCD" \
++        and "not_support1" not in prop and "not_supported2" not in prop
++    # parsing omitted value
++    prop = parser.parse_case_properities("[esp32]")
++    assert prop["ignore"] == "No" and prop["test_env"] == "UT_T1_1"
++    # parsing with incorrect format
++    try:
++        parser.parse_case_properities("abcd")
++        assert False
++    except AssertionError:
++        pass
++    # skip invalid data parse, [type=] assigns empty string to type
++    prop = parser.parse_case_properities("[esp32]abdc aaaa [ignore=]")
++    assert prop["module"] == "esp32" and prop["ignore"] == ""
++    # skip mis-paired []
++    prop = parser.parse_case_properities("[esp32][[ignore=b]][]][test_env=AAA]]")
++    assert prop["module"] == "esp32" and prop["ignore"] == "b" and prop["test_env"] == "AAA"
++
++
++def main():
++    test_parser()
++
++    idf_path = os.getenv("IDF_PATH")
++    elf_path = os.path.join(idf_path, "tools", "unit-test-app", "build", "unit-test-app.elf")
++
++    parser = Parser(idf_path)
++    parser.parse_test_cases_from_elf(elf_path)
++    parser.parse_gitlab_ci()
++    parser.dump_ci_config()
++    parser.copy_module_def_file()
++
++
++if __name__ == '__main__':
++    main()