Files
lvgl/tests/main.py
VIFEX 07539958a6
Some checks failed
Arduino Lint / lint (push) Has been cancelled
Build Examples with C++ Compiler / build-examples (push) Has been cancelled
MicroPython CI / Build esp32 port (push) Has been cancelled
MicroPython CI / Build rp2 port (push) Has been cancelled
MicroPython CI / Build stm32 port (push) Has been cancelled
MicroPython CI / Build unix port (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_NORMAL_8BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_SDL - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build ESP IDF ESP32S3 (push) Has been cancelled
C/C++ CI / Run tests with 32bit build (push) Has been cancelled
C/C++ CI / Run tests with 64bit build (push) Has been cancelled
BOM Check / bom-check (push) Has been cancelled
Verify that lv_conf_internal.h matches repository state / verify-conf-internal (push) Has been cancelled
Verify the widget property name / verify-property-name (push) Has been cancelled
Verify code formatting / verify-formatting (push) Has been cancelled
Compare file templates with file names / template-check (push) Has been cancelled
Build docs / build-and-deploy (push) Has been cancelled
Test API JSON generator / Test API JSON (push) Has been cancelled
Install LVGL using CMake / build-examples (push) Has been cancelled
Check Makefile / Build using Makefile (push) Has been cancelled
Check Makefile for UEFI / Build using Makefile for UEFI (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/benchmark_results_comment/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/filter_docker_logs/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/serialize_results/test.sh) (push) Has been cancelled
Hardware Performance Test / Hardware Performance Benchmark (push) Has been cancelled
Hardware Performance Test / HW Benchmark - Save PR Number (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_32B - Ubuntu (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_64B - Ubuntu (push) Has been cancelled
Port repo release update / run-release-branch-updater (push) Has been cancelled
Verify Font License / verify-font-license (push) Has been cancelled
Verify Kconfig / verify-kconfig (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 32b - lv_conf_perf32b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 64b - lv_conf_perf64b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Save PR Number (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
ci(tests): retain the gcov report for coverage analysis (#9274)
Signed-off-by: pengyiqiang <pengyiqiang@xiaomi.com>
Co-authored-by: pengyiqiang <pengyiqiang@xiaomi.com>
2025-11-21 22:45:47 +08:00

320 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import shutil
import subprocess
import sys
import os
import platform
from itertools import chain
from pathlib import Path
lvgl_test_dir = os.path.dirname(os.path.realpath(__file__))
lvgl_script_path = os.path.join(lvgl_test_dir, "../scripts")
sys.path.append(lvgl_script_path)
wayland_dir = os.path.join(lvgl_test_dir, "wayland_protocols")
wayland_protocols_dir = os.path.realpath("/usr/share/wayland-protocols")
from perf import perf_test_options
from LVGLImage import LVGLImage, ColorFormat, CompressMethod
# Key values must match variable names in CMakeLists.txt.
build_only_options = {
'OPTIONS_NORMAL_8BIT': 'Normal config, 8 bit color depth',
'OPTIONS_16BIT': 'Minimal config, 16 bit color depth',
'OPTIONS_24BIT': 'Normal config, 24 bit color depth',
'OPTIONS_FULL_32BIT': 'Full config, 32 bit color depth',
}
if platform.system() != 'Windows':
build_only_options['OPTIONS_SDL'] = 'SDL simulator with full config, 32 bit color depth'
test_options = {
'OPTIONS_TEST_SYSHEAP': 'Test config, system heap, 32 bit color depth',
'OPTIONS_TEST_DEFHEAP': 'Test config, LVGL heap, 32 bit color depth',
'OPTIONS_TEST_VG_LITE': 'VG-Lite simulator with full config, 32 bit color depth',
}
def get_option_description(option_name):
if option_name in build_only_options:
return build_only_options[option_name]
return test_options[option_name]
def delete_dir_ignore_missing(dir_path):
'''Recursively delete a directory and ignore if missing.'''
try:
shutil.rmtree(dir_path)
except FileNotFoundError:
pass
def options_abbrev(options_name):
'''Return an abbreviated version of the option name.'''
prefix = 'OPTIONS_'
assert options_name.startswith(prefix)
return options_name[len(prefix):].lower()
def get_base_build_dir(options_name):
'''Given the build options name, return the build directory name.
Does not return the full path to the directory - just the base name.'''
return 'build_%s' % options_abbrev(options_name)
def get_build_dir(options_name):
'''Given the build options name, return the build directory name.
Returns absolute path to the build directory.'''
global lvgl_test_dir
return os.path.join(lvgl_test_dir, get_base_build_dir(options_name))
def gen_wayland_protocols(clean):
'''Generates the xdg shell interface from wayland protocol definitions'''
if clean:
delete_dir_ignore_missing(wayland_dir)
if not os.path.isdir(wayland_dir):
os.mkdir(wayland_dir)
subprocess.check_call(['wayland-scanner',
'client-header',
os.path.join(wayland_protocols_dir, "stable/xdg-shell/xdg-shell.xml"),
os.path.join(wayland_dir, "wayland_xdg_shell.h.original"),
])
subprocess.check_call(['wayland-scanner',
'private-code',
os.path.join(wayland_protocols_dir, "stable/xdg-shell/xdg-shell.xml"),
os.path.join(wayland_dir, "wayland_xdg_shell.c.original"),
])
# Insert guards
with open(os.path.join(wayland_dir, "wayland_xdg_shell.h"), "w") as outfile:
subprocess.check_call(['sed','-e', "1i #if LV_BUILD_TEST", '-e', '$a #endif',
os.path.join(wayland_dir, "wayland_xdg_shell.h.original")], stdout=outfile)
with open(os.path.join(wayland_dir, "wayland_xdg_shell.c"), "w") as outfile:
subprocess.check_call(['sed','-e', "1i #if LV_BUILD_TEST", '-e', '$a #endif',
os.path.join(wayland_dir, "wayland_xdg_shell.c.original")], stdout=outfile)
subprocess.check_call(['rm', os.path.join(wayland_dir, "wayland_xdg_shell.c.original")])
subprocess.check_call(['rm', os.path.join(wayland_dir, "wayland_xdg_shell.h.original")])
def build_tests(options_name, build_type, clean):
'''Build all tests for the specified options name.'''
global lvgl_test_dir
print()
print()
label = 'Building: %s: %s' % (options_abbrev(
options_name), get_option_description(options_name))
print('=' * len(label))
print(label)
print('=' * len(label))
print(flush=True)
build_dir = get_build_dir(options_name)
if clean:
delete_dir_ignore_missing(build_dir)
os.chdir(lvgl_test_dir)
if platform.system() != 'Windows':
gen_wayland_protocols(clean)
created_build_dir = False
if not os.path.isdir(build_dir):
os.mkdir(build_dir)
created_build_dir = True
os.chdir(build_dir)
if created_build_dir:
subprocess.check_call(['cmake', '-GNinja', '-DCMAKE_BUILD_TYPE=%s' % build_type,
'-D%s=1' % options_name, '..'])
subprocess.check_call(['cmake', '--build', build_dir,
'--parallel', str(os.cpu_count())])
def run_tests(options_name, test_suite):
'''Run the tests for the given options name.'''
print()
print()
label = 'Running tests for %s' % options_abbrev(options_name)
print('=' * len(label))
print(label)
print('=' * len(label), flush=True)
os.chdir(get_build_dir(options_name))
args = [
'ctest',
'--timeout', '300',
'--parallel', str(os.cpu_count()),
'--output-on-failure',
]
if test_suite is not None:
args.extend(["--tests-regex", test_suite])
subprocess.check_call(args)
def generate_code_coverage_report():
'''Produce code coverage test reports for the test execution.'''
global lvgl_test_dir
print()
print()
label = 'Generating code coverage reports'
print('=' * len(label))
print(label)
print('=' * len(label))
print(flush=True)
os.chdir(lvgl_test_dir)
delete_dir_ignore_missing('report')
os.mkdir('report')
root_dir = os.path.dirname(lvgl_test_dir) # Get parent directory of tests (lvgl directory)
html_report_file = 'report/index.html'
cmd = ['gcovr', '--gcov-ignore-parse-errors',
'--root', root_dir, '--html-details', '--output',
html_report_file, '--xml', 'report/coverage.xml',
'-j', str(os.cpu_count()), '--print-summary', '--merge-mode-functions=merge-use-line-min',
'--html-title', 'LVGL Test Coverage', '--filter', os.path.join(root_dir, 'src/.*/lv_.*\\.c')]
subprocess.check_call(cmd)
print("Done: See %s" % html_report_file, flush=True)
def generate_test_images():
invalids = (ColorFormat.UNKNOWN,ColorFormat.RAW,ColorFormat.RAW_ALPHA)
formats = [i for i in ColorFormat if i not in invalids]
png_path = os.path.join(lvgl_test_dir, "test_images/pngs")
pngs = list(Path(png_path).rglob("*.[pP][nN][gG]"))
print(f"png files: {pngs}")
align_options = [1, 64]
for align in align_options:
for compress in CompressMethod:
compress_name = compress.name if compress != CompressMethod.NONE else "UNCOMPRESSED"
outputs = os.path.join(lvgl_test_dir, f"test_images/stride_align{align}/{compress_name}/")
os.makedirs(outputs, exist_ok=True)
for fmt in formats:
for png in pngs:
img = LVGLImage().from_png(png, cf=fmt, background=0xffffff)
img.adjust_stride(align=16)
output = os.path.join(outputs, f"{Path(png).stem}_{fmt.name}.bin")
img.to_bin(output, compress=compress)
output = os.path.join(outputs, f"{Path(png).stem}_{fmt.name}_{compress.name}_align{align}.c")
img.to_c_array(output, compress=compress)
print(f"converting {os.path.basename(png)}, format: {fmt.name}, compress: {compress_name}")
def clean_build_dirs_with_filter(build_dir, clean_filters):
for entry in os.listdir(build_dir):
entry_path = os.path.join(build_dir, entry)
if any(entry.endswith(filter) for filter in clean_filters):
continue
if os.path.isfile(entry_path):
os.remove(entry_path)
elif os.path.isdir(entry_path):
shutil.rmtree(entry_path)
if __name__ == "__main__":
epilog = '''This program builds and optionally runs the LVGL test programs.
There are two types of LVGL tests: "build", and "test". The build-only
tests, as their name suggests, only verify that the program successfully
compiles and links (with various build options). There are also a set of
tests that execute to verify correct LVGL library behavior.
'''
parser = argparse.ArgumentParser(
description='Build and/or run LVGL tests.', epilog=epilog)
parser.add_argument('--build-options', nargs=1,
choices=list(chain(build_only_options, test_options, perf_test_options)),
help='''the build option name to build or run. When
omitted all build configurations are used.
''')
parser.add_argument('--clean', action='store_true', default=False,
help='clean existing build artifacts before operation.')
parser.add_argument('--report', action='store_true',
help='generate code coverage report for tests.')
parser.add_argument('actions', nargs='*', choices=['build', 'test'],
help='build: compile build tests, test: compile/run executable tests.')
parser.add_argument('--test-suite', default=None,
help='select test suite to run')
parser.add_argument('--update-image', action='store_true', default=False,
help='Update test image using LVGLImage.py script')
parser.add_argument('--auto-clean', action='store_true', default=False,
help='Automatically clean build directories')
parser.add_argument('--keep-report', action='store_true', default=False,
help='Skip cleaning gcov report files when --auto-clean and --report are enabled')
args = parser.parse_args()
if args.update_image:
generate_test_images()
if args.build_options:
options_to_build = args.build_options
else:
if 'build' in args.actions:
if 'test' in args.actions:
options_to_build = {**build_only_options, **test_options}
else:
options_to_build = build_only_options
else:
options_to_build = test_options
clean_build_dirs = []
for options_name in options_to_build:
is_test = options_name in test_options
is_perf_test = options_name in perf_test_options
if is_perf_test:
perf_test_script = os.path.join(lvgl_test_dir, "perf.py")
try:
subprocess.check_call([perf_test_script, *(sys.argv[1:])])
except subprocess.CalledProcessError as e:
sys.exit(e.returncode)
continue
build_type = 'Debug'
build_tests(options_name, build_type, args.clean)
if is_test:
try:
run_tests(options_name, args.test_suite)
except subprocess.CalledProcessError as e:
sys.exit(e.returncode)
if args.auto_clean:
build_dir = get_build_dir(options_name)
if args.report:
# Keep the files that gcovr analysis depends on and delete
# the rest to solve the storage capacity limit of GitHub CI
clean_filters = ['CMakeFiles', '.c']
clean_build_dirs_with_filter(build_dir, clean_filters)
if args.keep_report:
# Retain the gcov report files for subsequent automated coverage analysis.
print(f"Keeping {build_dir} for report")
else:
print(f"Append {build_dir} to clean list")
clean_build_dirs.append(build_dir)
else:
# Remove all build directories directly
print(f"Removing {build_dir}")
shutil.rmtree(build_dir)
if args.report:
generate_code_coverage_report()
# Clean all build directories after report
for build_dir in clean_build_dirs:
print(f"Removing {build_dir}")
shutil.rmtree(build_dir)