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
- docs/_build/html
expire_in: 1 mos
--
test_nvs_on_host:
stage: test
image: $CI_DOCKER_REGISTRY/esp32-ci-env
- 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
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
#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 */
* 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); \
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:
--- /dev/null
- 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();
+}
+++ /dev/null
--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()
--- /dev/null
--- /dev/null
++# 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
--- /dev/null
--- /dev/null
++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"
--- /dev/null
--- /dev/null
++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()