1#!/usr/bin/env python3 2 3import argparse 4import errno 5import glob 6import shutil 7import subprocess 8import sys 9import os 10 11lvgl_test_dir = os.path.dirname(os.path.realpath(__file__)) 12 13# Key values must match variable names in CMakeLists.txt. 14build_only_options = { 15 'OPTIONS_MINIMAL_MONOCHROME': 'Minimal config monochrome', 16 'OPTIONS_NORMAL_8BIT': 'Normal config, 8 bit color depth', 17 'OPTIONS_16BIT': 'Minimal config, 16 bit color depth', 18 'OPTIONS_16BIT_SWAP': 'Normal config, 16 bit color depth swapped', 19 'OPTIONS_FULL_32BIT': 'Full config, 32 bit color depth', 20} 21 22test_options = { 23 'OPTIONS_TEST_SYSHEAP': 'Test config, system heap, 32 bit color depth', 24 'OPTIONS_TEST_DEFHEAP': 'Test config, LVGL heap, 32 bit color depth', 25} 26 27 28def is_valid_option_name(option_name): 29 return option_name in build_only_options or option_name in test_options 30 31 32def get_option_description(option_name): 33 if option_name in build_only_options: 34 return build_only_options[option_name] 35 return test_options[option_name] 36 37 38def delete_dir_ignore_missing(dir_path): 39 '''Recursively delete a directory and ignore if missing.''' 40 try: 41 shutil.rmtree(dir_path) 42 except FileNotFoundError: 43 pass 44 45 46def generate_test_runners(): 47 '''Generate the test runner source code.''' 48 global lvgl_test_dir 49 os.chdir(lvgl_test_dir) 50 delete_dir_ignore_missing('src/test_runners') 51 os.mkdir('src/test_runners') 52 53 # TODO: Intermediate files should be in the build folders, not alongside 54 # the other repo source. 55 for f in glob.glob("./src/test_cases/test_*.c"): 56 r = f[:-2] + "_Runner.c" 57 r = r.replace("/test_cases/", "/test_runners/") 58 subprocess.check_call(['ruby', 'unity/generate_test_runner.rb', 59 f, r, 'config.yml']) 60 61 62def options_abbrev(options_name): 63 '''Return an abbreviated version of the option name.''' 64 prefix = 'OPTIONS_' 65 assert options_name.startswith(prefix) 66 return options_name[len(prefix):].lower() 67 68 69def get_base_buid_dir(options_name): 70 '''Given the build options name, return the build directory name. 71 72 Does not return the full path to the directory - just the base name.''' 73 return 'build_%s' % options_abbrev(options_name) 74 75 76def get_build_dir(options_name): 77 '''Given the build options name, return the build directory name. 78 79 Returns absolute path to the build directory.''' 80 global lvgl_test_dir 81 return os.path.join(lvgl_test_dir, get_base_buid_dir(options_name)) 82 83 84def build_tests(options_name, build_type, clean): 85 '''Build all tests for the specified options name.''' 86 global lvgl_test_dir 87 88 print() 89 print() 90 label = 'Building: %s: %s' % (options_abbrev( 91 options_name), get_option_description(options_name)) 92 print('=' * len(label)) 93 print(label) 94 print('=' * len(label)) 95 print(flush=True) 96 97 build_dir = get_build_dir(options_name) 98 if clean: 99 delete_dir_ignore_missing(build_dir) 100 101 os.chdir(lvgl_test_dir) 102 created_build_dir = False 103 if not os.path.isdir(build_dir): 104 os.mkdir(build_dir) 105 created_build_dir = True 106 os.chdir(build_dir) 107 if created_build_dir: 108 subprocess.check_call(['cmake', '-DCMAKE_BUILD_TYPE=%s' % build_type, 109 '-D%s=1' % options_name, '..']) 110 subprocess.check_call(['cmake', '--build', build_dir, 111 '--parallel', str(os.cpu_count())]) 112 113 114def run_tests(options_name): 115 '''Run the tests for the given options name.''' 116 117 print() 118 print() 119 label = 'Running tests for %s' % options_abbrev(options_name) 120 print('=' * len(label)) 121 print(label) 122 print('=' * len(label), flush=True) 123 124 os.chdir(get_build_dir(options_name)) 125 subprocess.check_call( 126 ['ctest', '--timeout', '30', '--parallel', str(os.cpu_count()), '--output-on-failure']) 127 128 129def generate_code_coverage_report(): 130 '''Produce code coverage test reports for the test execution.''' 131 global lvgl_test_dir 132 133 print() 134 print() 135 label = 'Generating code coverage reports' 136 print('=' * len(label)) 137 print(label) 138 print('=' * len(label)) 139 print(flush=True) 140 141 os.chdir(lvgl_test_dir) 142 delete_dir_ignore_missing('report') 143 os.mkdir('report') 144 root_dir = os.pardir 145 html_report_file = 'report/index.html' 146 cmd = ['gcovr', '--root', root_dir, '--html-details', '--output', 147 html_report_file, '--xml', 'report/coverage.xml', 148 '-j', str(os.cpu_count()), '--print-summary', 149 '--html-title', 'LVGL Test Coverage'] 150 for d in ('.*\\bexamples/.*', '\\bsrc/test_.*', '\\bsrc/lv_test.*', '\\bunity\\b'): 151 cmd.extend(['--exclude', d]) 152 153 subprocess.check_call(cmd) 154 print("Done: See %s" % html_report_file, flush=True) 155 156 157if __name__ == "__main__": 158 epilog = '''This program builds and optionally runs the LVGL test programs. 159 There are two types of LVGL tests: "build", and "test". The build-only 160 tests, as their name suggests, only verify that the program successfully 161 compiles and links (with various build options). There are also a set of 162 tests that execute to verify correct LVGL library behavior. 163 ''' 164 parser = argparse.ArgumentParser( 165 description='Build and/or run LVGL tests.', epilog=epilog) 166 parser.add_argument('--build-options', nargs=1, 167 help='''the build option name to build or run. When 168 omitted all build configurations are used. 169 ''') 170 parser.add_argument('--clean', action='store_true', default=False, 171 help='clean existing build artifacts before operation.') 172 parser.add_argument('--report', action='store_true', 173 help='generate code coverage report for tests.') 174 parser.add_argument('actions', nargs='*', choices=['build', 'test'], 175 help='build: compile build tests, test: compile/run executable tests.') 176 177 args = parser.parse_args() 178 179 if args.build_options: 180 options_to_build = args.build_options 181 else: 182 if 'build' in args.actions: 183 if 'test' in args.actions: 184 options_to_build = {**build_only_options, **test_options} 185 else: 186 options_to_build = build_only_options 187 else: 188 options_to_build = test_options 189 190 for opt in options_to_build: 191 if not is_valid_option_name(opt): 192 print('Invalid build option "%s"' % opt, file=sys.stderr) 193 sys.exit(errno.EINVAL) 194 195 generate_test_runners() 196 197 for options_name in options_to_build: 198 is_test = options_name in test_options 199 build_type = 'Debug' 200 build_tests(options_name, build_type, args.clean) 201 if is_test: 202 try: 203 run_tests(options_name) 204 except subprocess.CalledProcessError as e: 205 sys.exit(e.returncode) 206 207 if args.report: 208 generate_code_coverage_report() 209