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