ci: extract ElfUnitTestParser allowing resolve elf offline

pull/9068/head
Michael (XIAO Xufeng) 2022-05-23 15:06:46 +08:00
rodzic 5607018f28
commit cd6056c9cd
3 zmienionych plików z 122 dodań i 57 usunięć

Wyświetl plik

@ -13,6 +13,7 @@ ESP-IDF unit tests are run using Unit Test App. The app can be built with the un
* `idf.py -T <component> -T <component> ... build` with `component` set to names of the components to be included in the test app. Or `idf.py -T all build` to build the test app with all the tests for components having `test` subdirectory. * `idf.py -T <component> -T <component> ... build` with `component` set to names of the components to be included in the test app. Or `idf.py -T all build` to build the test app with all the tests for components having `test` subdirectory.
* Follow the printed instructions to flash, or run `idf.py -p PORT flash`. * Follow the printed instructions to flash, or run `idf.py -p PORT flash`.
* Unit test have a few preset sdkconfigs. It provides command `idf.py ut-clean-config_name` and `idf.py ut-build-config_name` (where `config_name` is the file name under `unit-test-app/configs` folder) to build with preset configs. For example, you can use `idf.py -T all ut-build-default` to build with config file `unit-test-app/configs/default`. Built binary for this config will be copied to `unit-test-app/output/config_name` folder. * Unit test have a few preset sdkconfigs. It provides command `idf.py ut-clean-config_name` and `idf.py ut-build-config_name` (where `config_name` is the file name under `unit-test-app/configs` folder) to build with preset configs. For example, you can use `idf.py -T all ut-build-default` to build with config file `unit-test-app/configs/default`. Built binary for this config will be copied to `unit-test-app/output/config_name` folder.
* You may extract the test cases presented in the built elf file by calling `ElfUnitTestParser.py <your_elf>`.
# Flash Size # Flash Size
@ -38,7 +39,7 @@ Unit test uses 3 stages in CI: `build`, `assign_test`, `unit_test`.
### Build Stage: ### Build Stage:
`build_esp_idf_tests` job will build all UT configs and parse test cases form built elf files. Built binary (`tools/unit-test-app/output`) and parsed cases (`components/idf_test/unit_test/TestCaseAll.yml`) will be saved as artifacts. `build_esp_idf_tests` job will build all UT configs and run script `UnitTestParser.py` to parse test cases form built elf files. Built binary (`tools/unit-test-app/output`) and parsed cases (`components/idf_test/unit_test/TestCaseAll.yml`) will be saved as artifacts.
When we add new test case, it will construct a structure to save case data during build. We'll parse the test case from this structure. The description (defined in test case: `TEST_CASE("name", "description")`) is used to extend test case definition. The format of test description is a list of tags: When we add new test case, it will construct a structure to save case data during build. We'll parse the test case from this structure. The description (defined in test case: `TEST_CASE("name", "description")`) is used to extend test case definition. The format of test description is a list of tags:
@ -117,6 +118,7 @@ If you want to reproduce locally, you need to:
* You can refer to [unit test document](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/unit-tests.html#running-unit-tests) to run test manually. * You can refer to [unit test document](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/unit-tests.html#running-unit-tests) to run test manually.
* Or, you can use `tools/unit-test-app/unit_test.py` to run the test cases (see below) * Or, you can use `tools/unit-test-app/unit_test.py` to run the test cases (see below)
# Testing and debugging on local machine
## Running unit tests on local machine by `unit_test.py` ## Running unit tests on local machine by `unit_test.py`
First, install Python dependencies and export the Python path where the IDF CI Python modules are found: First, install Python dependencies and export the Python path where the IDF CI Python modules are found:

Wyświetl plik

@ -0,0 +1,83 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import os
import subprocess
import sys
from typing import Dict, List
import yaml
try:
import CreateSectionTable
except ImportError:
sys.path.append(os.path.expandvars(os.path.join('$IDF_PATH', 'tools', 'unit-test-app', 'tools')))
import CreateSectionTable
def get_target_objdump(idf_target: str) -> str:
toolchain_for_target = {
'esp32': 'xtensa-esp32-elf-',
'esp32s2': 'xtensa-esp32s2-elf-',
'esp32s3': 'xtensa-esp32s3-elf-',
'esp32c2': 'riscv32-esp-elf-',
'esp32c3': 'riscv32-esp-elf-',
}
return toolchain_for_target.get(idf_target, '') + 'objdump'
def parse_elf_test_cases(elf_file: str, idf_target: str) -> List[Dict]:
objdump = get_target_objdump(idf_target)
try:
subprocess.check_output('{} -s {} > section_table.tmp'.format(objdump, elf_file), shell=True)
table = CreateSectionTable.SectionTable('section_table.tmp')
except subprocess.CalledProcessError:
raise Exception('Can\'t resolve elf file. File not found.')
finally:
os.remove('section_table.tmp')
bin_test_cases = []
try:
subprocess.check_output('{} -t {} | grep test_desc > case_address.tmp'.format(objdump, elf_file),
shell=True)
with open('case_address.tmp', 'rb') as input_f:
for line in input_f:
# process symbol table like: "3ffb4310 l O .dram0.data 00000018 test_desc_33$5010"
sections = line.split()
test_addr = int(sections[0], 16)
section = sections[3]
name_addr = table.get_unsigned_int(section, test_addr, 4)
desc_addr = table.get_unsigned_int(section, test_addr + 4, 4)
tc = {
'name': table.get_string('any', name_addr),
'desc': table.get_string('any', desc_addr),
'function_count': table.get_unsigned_int(section, test_addr + 20, 4),
}
bin_test_cases.append(tc)
except subprocess.CalledProcessError:
raise Exception('Test cases not found')
finally:
os.remove('case_address.tmp')
return bin_test_cases
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('elf_file', help='Elf file to parse')
parser.add_argument('-t', '--idf_target',
type=str, default=os.environ.get('IDF_TARGET', ''),
help='Target of the elf, e.g. esp32s2')
parser.add_argument('-o', '--output_file',
type=str, default='elf_test_cases.yml',
help='Target of the elf, e.g. esp32s2')
args = parser.parse_args()
assert args.idf_target
test_cases = parse_elf_test_cases(args.elf_file, args.idf_target)
with open(args.output_file, 'w') as out_file:
yaml.dump(test_cases, out_file, default_flow_style=False)

Wyświetl plik

@ -4,10 +4,9 @@ import argparse
import os import os
import re import re
import shutil import shutil
import subprocess import sys
from copy import deepcopy from copy import deepcopy
import CreateSectionTable
import yaml import yaml
try: try:
@ -15,6 +14,13 @@ try:
except ImportError: except ImportError:
from yaml import Loader as Loader # type: ignore from yaml import Loader as Loader # type: ignore
try:
from ElfUnitTestParser import parse_elf_test_cases
except ImportError:
sys.path.append(os.path.expandvars(os.path.join('$IDF_PATH', 'tools', 'unit-test-app', 'tools')))
from ElfUnitTestParser import parse_elf_test_cases
TEST_CASE_PATTERN = { TEST_CASE_PATTERN = {
'initial condition': 'UTINIT1', 'initial condition': 'UTINIT1',
'chip_target': 'esp32', 'chip_target': 'esp32',
@ -50,12 +56,6 @@ class Parser(object):
ELF_FILE = 'unit-test-app.elf' ELF_FILE = 'unit-test-app.elf'
SDKCONFIG_FILE = 'sdkconfig' SDKCONFIG_FILE = 'sdkconfig'
STRIP_CONFIG_PATTERN = re.compile(r'(.+?)(_\d+)?$') STRIP_CONFIG_PATTERN = re.compile(r'(.+?)(_\d+)?$')
TOOLCHAIN_FOR_TARGET = {
'esp32': 'xtensa-esp32-elf-',
'esp32s2': 'xtensa-esp32s2-elf-',
'esp32s3': 'xtensa-esp32s3-elf-',
'esp32c3': 'riscv32-esp-elf-',
}
def __init__(self, binary_folder, node_index): def __init__(self, binary_folder, node_index):
idf_path = os.getenv('IDF_PATH') idf_path = os.getenv('IDF_PATH')
@ -67,7 +67,6 @@ class Parser(object):
self.idf_target = idf_target self.idf_target = idf_target
self.node_index = node_index self.node_index = node_index
self.ut_bin_folder = binary_folder self.ut_bin_folder = binary_folder
self.objdump = Parser.TOOLCHAIN_FOR_TARGET.get(idf_target, '') + 'objdump'
self.tag_def = yaml.load(open(os.path.join(idf_path, self.TAG_DEF_FILE), 'r'), Loader=Loader) self.tag_def = yaml.load(open(os.path.join(idf_path, self.TAG_DEF_FILE), 'r'), Loader=Loader)
self.module_map = yaml.load(open(os.path.join(idf_path, self.MODULE_DEF_FILE), 'r'), Loader=Loader) self.module_map = yaml.load(open(os.path.join(idf_path, self.MODULE_DEF_FILE), 'r'), Loader=Loader)
self.config_dependencies = yaml.load(open(os.path.join(idf_path, self.CONFIG_DEPENDENCY_FILE), 'r'), self.config_dependencies = yaml.load(open(os.path.join(idf_path, self.CONFIG_DEPENDENCY_FILE), 'r'),
@ -89,32 +88,16 @@ class Parser(object):
test_groups = self.get_test_groups(os.path.join(configs_folder, config_name)) test_groups = self.get_test_groups(os.path.join(configs_folder, config_name))
elf_file = os.path.join(config_output_folder, self.ELF_FILE) elf_file = os.path.join(config_output_folder, self.ELF_FILE)
subprocess.check_output('{} -t {} | grep test_desc > case_address.tmp'.format(self.objdump, elf_file), bin_test_cases = parse_elf_test_cases(elf_file, self.idf_target)
shell=True)
subprocess.check_output('{} -s {} > section_table.tmp'.format(self.objdump, elf_file), shell=True)
table = CreateSectionTable.SectionTable('section_table.tmp')
test_cases = [] test_cases = []
for bin_tc in bin_test_cases:
# we could split cases of same config into multiple binaries as we have limited rom space # we could split cases of same config into multiple binaries as we have limited rom space
# we should regard those configs like `default` and `default_2` as the same config # we should regard those configs like `default` and `default_2` as the same config
match = self.STRIP_CONFIG_PATTERN.match(config_name) match = self.STRIP_CONFIG_PATTERN.match(config_name)
stripped_config_name = match.group(1) stripped_config_name = match.group(1)
with open('case_address.tmp', 'rb') as f: tc = self.parse_one_test_case(bin_tc['name'], bin_tc['desc'], config_name, stripped_config_name, tags)
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)
function_count = table.get_unsigned_int(section, test_addr + 20, 4)
name = table.get_string('any', name_addr)
desc = table.get_string('any', desc_addr)
tc = self.parse_one_test_case(name, desc, config_name, stripped_config_name, tags)
# check if duplicated case names # check if duplicated case names
# we need to use it to select case, # we need to use it to select case,
@ -137,15 +120,12 @@ class Parser(object):
else: else:
self.test_env_tags.update({tc['test environment']: [tc['ID']]}) self.test_env_tags.update({tc['test environment']: [tc['ID']]})
if function_count > 1: if bin_tc['function_count'] > 1:
tc.update({'child case num': function_count}) tc.update({'child case num': bin_tc['function_count']})
# only add cases need to be executed # only add cases need to be executed
test_cases.append(tc) test_cases.append(tc)
os.remove('section_table.tmp')
os.remove('case_address.tmp')
return test_cases return test_cases
def parse_case_properties(self, tags_raw): def parse_case_properties(self, tags_raw):