1#!/usr/bin/env python3 2 3import argparse 4import errno 5import shutil 6import subprocess 7import sys 8import os 9import platform 10from itertools import chain 11from pathlib import Path 12 13lvgl_test_dir = os.path.dirname(os.path.realpath(__file__)) 14lvgl_script_path = os.path.join(lvgl_test_dir, "../scripts") 15sys.path.append(lvgl_script_path) 16 17wayland_dir = os.path.join(lvgl_test_dir, "wayland_protocols") 18wayland_protocols_dir = os.path.realpath("/usr/share/wayland-protocols") 19 20from LVGLImage import LVGLImage, ColorFormat, CompressMethod 21 22# Key values must match variable names in CMakeLists.txt. 23build_only_options = { 24# 'OPTIONS_NORMAL_8BIT': 'Normal config, 8 bit color depth', 25 'OPTIONS_16BIT': 'Minimal config, 16 bit color depth', 26 'OPTIONS_24BIT': 'Normal config, 24 bit color depth', 27 'OPTIONS_FULL_32BIT': 'Full config, 32 bit color depth', 28 'OPTIONS_VG_LITE': 'VG-Lite simulator with full config, 32 bit color depth', 29} 30 31if platform.system() != 'Windows': 32 build_only_options['OPTIONS_SDL'] = 'SDL simulator with full config, 32 bit color depth' 33 34test_options = { 35 'OPTIONS_TEST_SYSHEAP': 'Test config, system heap, 32 bit color depth', 36 'OPTIONS_TEST_DEFHEAP': 'Test config, LVGL heap, 32 bit color depth', 37 'OPTIONS_TEST_VG_LITE': 'VG-Lite simulator with full config, 32 bit color depth', 38} 39 40 41def get_option_description(option_name): 42 if option_name in build_only_options: 43 return build_only_options[option_name] 44 return test_options[option_name] 45 46 47def delete_dir_ignore_missing(dir_path): 48 '''Recursively delete a directory and ignore if missing.''' 49 try: 50 shutil.rmtree(dir_path) 51 except FileNotFoundError: 52 pass 53 54 55def options_abbrev(options_name): 56 '''Return an abbreviated version of the option name.''' 57 prefix = 'OPTIONS_' 58 assert options_name.startswith(prefix) 59 return options_name[len(prefix):].lower() 60 61 62def get_base_build_dir(options_name): 63 '''Given the build options name, return the build directory name. 64 65 Does not return the full path to the directory - just the base name.''' 66 return 'build_%s' % options_abbrev(options_name) 67 68 69def get_build_dir(options_name): 70 '''Given the build options name, return the build directory name. 71 72 Returns absolute path to the build directory.''' 73 global lvgl_test_dir 74 return os.path.join(lvgl_test_dir, get_base_build_dir(options_name)) 75 76def gen_wayland_protocols(clean): 77 '''Generates the xdg shell interface from wayland protocol definitions''' 78 79 if clean: 80 delete_dir_ignore_missing(wayland_dir) 81 82 if not os.path.isdir(wayland_dir): 83 os.mkdir(wayland_dir) 84 subprocess.check_call(['wayland-scanner', 85 'client-header', 86 os.path.join(wayland_protocols_dir, "stable/xdg-shell/xdg-shell.xml"), 87 os.path.join(wayland_dir, "wayland_xdg_shell.h.original"), 88 ]) 89 90 subprocess.check_call(['wayland-scanner', 91 'private-code', 92 os.path.join(wayland_protocols_dir, "stable/xdg-shell/xdg-shell.xml"), 93 os.path.join(wayland_dir, "wayland_xdg_shell.c.original"), 94 ]) 95 96 # Insert guards 97 with open(os.path.join(wayland_dir, "wayland_xdg_shell.h"), "w") as outfile: 98 subprocess.check_call(['sed','-e', "1i #if LV_BUILD_TEST", '-e', '$a #endif', 99 os.path.join(wayland_dir, "wayland_xdg_shell.h.original")], stdout=outfile) 100 101 with open(os.path.join(wayland_dir, "wayland_xdg_shell.c"), "w") as outfile: 102 subprocess.check_call(['sed','-e', "1i #if LV_BUILD_TEST", '-e', '$a #endif', 103 os.path.join(wayland_dir, "wayland_xdg_shell.c.original")], stdout=outfile) 104 105 subprocess.check_call(['rm', os.path.join(wayland_dir, "wayland_xdg_shell.c.original")]) 106 subprocess.check_call(['rm', os.path.join(wayland_dir, "wayland_xdg_shell.h.original")]) 107 108def build_tests(options_name, build_type, clean): 109 '''Build all tests for the specified options name.''' 110 global lvgl_test_dir 111 112 print() 113 print() 114 label = 'Building: %s: %s' % (options_abbrev( 115 options_name), get_option_description(options_name)) 116 print('=' * len(label)) 117 print(label) 118 print('=' * len(label)) 119 print(flush=True) 120 121 build_dir = get_build_dir(options_name) 122 if clean: 123 delete_dir_ignore_missing(build_dir) 124 125 os.chdir(lvgl_test_dir) 126 127 if platform.system() != 'Windows': 128 gen_wayland_protocols(clean) 129 130 created_build_dir = False 131 if not os.path.isdir(build_dir): 132 os.mkdir(build_dir) 133 created_build_dir = True 134 os.chdir(build_dir) 135 if created_build_dir: 136 subprocess.check_call(['cmake', '-GNinja', '-DCMAKE_BUILD_TYPE=%s' % build_type, 137 '-D%s=1' % options_name, '..']) 138 subprocess.check_call(['cmake', '--build', build_dir, 139 '--parallel', str(os.cpu_count())]) 140 141 142def run_tests(options_name, test_suite): 143 '''Run the tests for the given options name.''' 144 145 print() 146 print() 147 label = 'Running tests for %s' % options_abbrev(options_name) 148 print('=' * len(label)) 149 print(label) 150 print('=' * len(label), flush=True) 151 152 os.chdir(get_build_dir(options_name)) 153 args = [ 154 'ctest', 155 '--timeout', '300', 156 '--parallel', str(os.cpu_count()), 157 '--output-on-failure', 158 ] 159 if test_suite is not None: 160 args.extend(["--tests-regex", test_suite]) 161 subprocess.check_call(args) 162 163 164def generate_code_coverage_report(): 165 '''Produce code coverage test reports for the test execution.''' 166 global lvgl_test_dir 167 168 print() 169 print() 170 label = 'Generating code coverage reports' 171 print('=' * len(label)) 172 print(label) 173 print('=' * len(label)) 174 print(flush=True) 175 176 os.chdir(lvgl_test_dir) 177 delete_dir_ignore_missing('report') 178 os.mkdir('report') 179 root_dir = os.pardir 180 html_report_file = 'report/index.html' 181 cmd = ['gcovr', '--root', root_dir, '--html-details', '--output', 182 html_report_file, '--xml', 'report/coverage.xml', 183 '-j', str(os.cpu_count()), '--print-summary', 184 '--html-title', 'LVGL Test Coverage', '--filter', r'../src/.*/lv_.*\.c'] 185 186 subprocess.check_call(cmd) 187 print("Done: See %s" % html_report_file, flush=True) 188 189 190def generate_test_images(): 191 invalids = (ColorFormat.UNKNOWN,ColorFormat.RAW,ColorFormat.RAW_ALPHA) 192 formats = [i for i in ColorFormat if i not in invalids] 193 png_path = os.path.join(lvgl_test_dir, "test_images/pngs") 194 pngs = list(Path(png_path).rglob("*.[pP][nN][gG]")) 195 print(f"png files: {pngs}") 196 197 align_options = [1, 64] 198 199 for align in align_options: 200 for compress in CompressMethod: 201 compress_name = compress.name if compress != CompressMethod.NONE else "UNCOMPRESSED" 202 outputs = os.path.join(lvgl_test_dir, f"test_images/stride_align{align}/{compress_name}/") 203 os.makedirs(outputs, exist_ok=True) 204 for fmt in formats: 205 for png in pngs: 206 img = LVGLImage().from_png(png, cf=fmt, background=0xffffff) 207 img.adjust_stride(align=16) 208 output = os.path.join(outputs, f"{Path(png).stem}_{fmt.name}.bin") 209 img.to_bin(output, compress=compress) 210 output = os.path.join(outputs, f"{Path(png).stem}_{fmt.name}_{compress.name}_align{align}.c") 211 img.to_c_array(output, compress=compress) 212 print(f"converting {os.path.basename(png)}, format: {fmt.name}, compress: {compress_name}") 213 214 215if __name__ == "__main__": 216 epilog = '''This program builds and optionally runs the LVGL test programs. 217 There are two types of LVGL tests: "build", and "test". The build-only 218 tests, as their name suggests, only verify that the program successfully 219 compiles and links (with various build options). There are also a set of 220 tests that execute to verify correct LVGL library behavior. 221 ''' 222 parser = argparse.ArgumentParser( 223 description='Build and/or run LVGL tests.', epilog=epilog) 224 parser.add_argument('--build-options', nargs=1, 225 choices=list(chain(build_only_options, test_options)), 226 help='''the build option name to build or run. When 227 omitted all build configurations are used. 228 ''') 229 parser.add_argument('--clean', action='store_true', default=False, 230 help='clean existing build artifacts before operation.') 231 parser.add_argument('--report', action='store_true', 232 help='generate code coverage report for tests.') 233 parser.add_argument('actions', nargs='*', choices=['build', 'test'], 234 help='build: compile build tests, test: compile/run executable tests.') 235 parser.add_argument('--test-suite', default=None, 236 help='select test suite to run') 237 parser.add_argument('--update-image', action='store_true', default=False, 238 help='Update test image using LVGLImage.py script') 239 240 args = parser.parse_args() 241 242 if args.update_image: 243 generate_test_images() 244 245 if args.build_options: 246 options_to_build = args.build_options 247 else: 248 if 'build' in args.actions: 249 if 'test' in args.actions: 250 options_to_build = {**build_only_options, **test_options} 251 else: 252 options_to_build = build_only_options 253 else: 254 options_to_build = test_options 255 256 for options_name in options_to_build: 257 is_test = options_name in test_options 258 build_type = 'Debug' 259 build_tests(options_name, build_type, args.clean) 260 if is_test: 261 try: 262 run_tests(options_name, args.test_suite) 263 except subprocess.CalledProcessError as e: 264 sys.exit(e.returncode) 265 266 if args.report: 267 generate_code_coverage_report() 268