From: wangmengyang Date: Mon, 10 Apr 2017 08:12:21 +0000 (+0800) Subject: component/bt: implement classic Bluetooth profiles A2DP(sink) and AVRCP(controller) X-Git-Tag: v2.1-rc1~196^2~1 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=ccfe4fefeb11dd577647168219cf070bbd1b312f;p=esp-idf component/bt: implement classic Bluetooth profiles A2DP(sink) and AVRCP(controller) --- ccfe4fefeb11dd577647168219cf070bbd1b312f diff --cc .gitlab-ci.yml index ef0635139e,ef0635139e..14c8702676 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@@ -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 @@@ -380,7 -380,7 +393,7 @@@ 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 diff --cc components/bt/Kconfig index d9021e9601,5b2cc3e6aa..eaa3d1625f --- a/components/bt/Kconfig +++ b/components/bt/Kconfig @@@ -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. + #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 ++ 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 diff --cc components/esp32/cpu_start.c index 495a178034,9c06a49f26..e0ad356ab9 --- a/components/esp32/cpu_start.c +++ b/components/esp32/cpu_start.c @@@ -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 */ diff --cc components/esp32/include/esp_err.h index c7beafd375,c7beafd375..2990c8938e --- a/components/esp32/include/esp_err.h +++ b/components/esp32/include/esp_err.h @@@ -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); \ diff --cc components/idf_test/unit_test/InitialConditionAll.yml index 3821894552,3821894552..48045c286d --- a/components/idf_test/unit_test/InitialConditionAll.yml +++ b/components/idf_test/unit_test/InitialConditionAll.yml @@@ -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 @@@ -2948,6 -2948,6 +2954,8 @@@ - '' - - FREBOOT UT1 - [''] ++ - - DELAY 3 ++ - [''] - - UT UT1 - - [R UT1 C Tests C Failures C Ignored] restore post cmd set: diff --cc examples/bluetooth/a2dp_sink/main/demo_main.c index 8a90e6b5a3,0000000000..3511a3007a mode 100755,000000..100755 --- a/examples/bluetooth/a2dp_sink/main/demo_main.c +++ b/examples/bluetooth/a2dp_sink/main/demo_main.c @@@ -1,25 -1,0 +1,38 @@@ +#include +#include +#include +#include +#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(); - esp_bt_controller_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 d3fa1efb02,d3fa1efb02..0000000000 deleted file mode 100644,100644 --- a/tools/unit-test-app/UnitTestParser.py +++ /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() diff --cc tools/unit-test-app/tools/CreateSectionTable.py index 0000000000,0000000000..a9379cf049 new file mode 100644 --- /dev/null +++ b/tools/unit-test-app/tools/CreateSectionTable.py @@@ -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 diff --cc tools/unit-test-app/tools/TagDefinition.yml index 0000000000,0000000000..3b952eaed6 new file mode 100644 --- /dev/null +++ b/tools/unit-test-app/tools/TagDefinition.yml @@@ -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" diff --cc tools/unit-test-app/tools/UnitTestParser.py index 0000000000,0000000000..0f151e9d92 new file mode 100644 --- /dev/null +++ b/tools/unit-test-app/tools/UnitTestParser.py @@@ -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()