1#!/usr/bin/env python3
2#
3# Arm SCP/MCP Software
4# Copyright (c) 2021-2023, Arm Limited and Contributors. All rights reserved.
5#
6# SPDX-License-Identifier: BSD-3-Clause
7#
8
9import argparse
10import check_copyright
11import check_doc
12import check_spacing
13import check_tabs
14import check_framework
15import check_module_utest
16import check_pycodestyle
17import docker
18import os
19import signal
20import subprocess
21import sys
22from product import Product, Build, Parameter
23from typing import List, Tuple
24
25from docker.errors import DockerException
26
27
28code_validations = [
29    check_copyright,
30    check_spacing,
31    check_tabs,
32    check_doc,
33    check_framework,
34    check_module_utest,
35    check_pycodestyle,
36]
37
38products = [
39    Product('host', toolchains=[Parameter('GNU')]),
40    Product('juno'),
41    Product('morello'),
42    Product('n1sdp'),
43    Product('rdv1'),
44    Product('rdv1mc'),
45    Product('rdn1e1'),
46    Product('sgi575'),
47    Product('sgm775'),
48    Product('sgm776'),
49    Product('synquacer'),
50    Product('tc1'),
51    Product('rcar', toolchains=[Parameter('GNU')]),
52    Product('rdn2', variants=[
53        Parameter('0'), Parameter('1'), Parameter('2'), Parameter('3')]),
54    Product('rdfremont'),
55    Product('tc2', variants=[Parameter('0'), Parameter('1')]),
56]
57
58
59def banner(text):
60    columns = 80
61    title = " {} ".format(text)
62    print("\n\n{}".format(title.center(columns, "*")))
63
64
65def dockerize(client):
66    (image, _) = client.images.build(path="docker", target="dev", rm=True)
67
68    env = {"ARMLMD_LICENSE_FILE": os.environ["ARMLMD_LICENSE_FILE"]}
69
70    volumes = {
71        os.getcwd(): {
72            "bind": "/scp-firmware",
73            "mode": "rw",
74        }
75    }
76
77    container = client.containers.run(
78        image.id, __file__, environment=env, volumes=volumes, detach=True,
79        tty=True
80    )
81
82    sigint_handler = signal.getsignal(signal.SIGINT)
83    signal.signal(signal.SIGINT, lambda signal, _: container.kill(signal))
84
85    logs = container.attach(stdout=True, stderr=True, stream=True, logs=True)
86    for log in logs:
87        sys.stdout.buffer.write(log)
88
89    signal.signal(signal.SIGINT, sigint_handler)
90
91
92def code_validation(checks: list) -> List[Tuple[str, int]]:
93    banner('Code validation')
94    results: List[Tuple[str, int]] = []
95    for check in checks:
96        result = check.main()
97        test_name = check.__name__.split('_')[-1]
98        results.append(('Check {}'.format(test_name), result))
99    return results
100
101
102def do_build(build_info: List[Build], output_path: str) -> \
103        List[Tuple[Build, subprocess.Popen]]:
104    build_status: List[Tuple[Build, subprocess.Popen]] = []
105    results: List[Tuple[str, int]] = []
106
107    if not os.path.exists(output_path):
108        os.makedirs(output_path)
109
110    files = []
111    for build in build_info:
112        file_path = os.path.join(output_path, build.file_name())
113        files.append(open(file_path, "w", encoding="utf-8"))
114
115        build_id = subprocess.Popen(
116                                build.command(),
117                                shell=True,
118                                stdout=files[-1],
119                                stderr=subprocess.STDOUT)
120
121        build_status.append((build, build_id))
122        print('Test building [{}]'.format(build.tag()))
123        print('[CMD] {}'.format(build.command()))
124
125    for i, (build, build_id) in enumerate(build_status):
126        build_id.communicate()
127        results.append((build.tag(), build_id.returncode))
128        files[i].close()
129    return results
130
131
132def print_results(results: List[Tuple[str, int]]) -> Tuple[int, int]:
133    banner('Tests summary')
134    total_success = 0
135    for result in results:
136        if result[1] == 0:
137            total_success += 1
138            verbose_result = "Success"
139        else:
140            verbose_result = "Failed"
141        print("{}: {}".format(result[0], verbose_result))
142
143    assert total_success <= len(results)
144    return (total_success, len(results))
145
146
147def analyze_results(success: int, total: int) -> int:
148    print("{} / {} passed ({}% pass rate)".format(success, total,
149                                                  int(success * 100 / total)))
150    return 1 if success < total else 0
151
152
153def check_errors(ignore_errors: bool, results: List[Tuple[str, int]]) -> bool:
154    return not ignore_errors and len(list(filter(lambda x: x[1] != 0,
155                                                 results)))
156
157
158def main(ignore_errors: bool, skip_container: bool, log_level: str,
159         output_path: str):
160    # This code is only applicable if there is valid docker instance
161    # On CI there is no docker instance at the moment
162    if not skip_container:
163        try:
164            client = docker.from_env()
165            banner("Spawning container")
166
167            try:
168                return dockerize(client)
169            except Exception as ex:
170                print(ex)
171                return 1
172
173        except DockerException:
174            pass
175    else:
176        banner("Skipping spawning container")
177
178    results = []
179
180    results.extend(code_validation(code_validations))
181    if check_errors(ignore_errors, results):
182        print('Errors detected! Excecution stopped')
183        return analyze_results(*print_results(results))
184
185    banner('Test building products')
186
187    if output_path == "":
188        output_path = os.path.join("/tmp", "scp")
189    output_path = os.path.join(output_path, "build-output")
190
191    for product in products:
192        if log_level != "":
193            product.log_level = Parameter(log_level)
194        results.extend(do_build(product.builds, output_path))
195        if check_errors(ignore_errors, results):
196            print('Errors detected! Excecution stopped')
197            return analyze_results(*print_results(results))
198
199    return analyze_results(*print_results(results))
200
201
202def parse_args():
203    parser = argparse.ArgumentParser(
204        description='Perform basic checks to SCP-Firmware and build for all \
205                     supported platforms, modes and compilers.')
206
207    parser.add_argument('-i', '--ignore-errors', dest='ignore_errors',
208                        required=False, default=False, action='store_true',
209                        help='Ignore errors and continue testing.')
210
211    parser.add_argument('-ll', '--log-level', dest='log_level',
212                        required=False, default="", type=str,
213                        action='store', help='Build every product with the \
214                        specified log level.')
215
216    parser.add_argument('-bod', '--build-output-dir', dest='output_path',
217                        required=False, default="", type=str, action='store',
218                        help='Parent directory of the "build-output" directory\
219                        , the one were the build logs will be stored in.\n \
220                        If bod is not given, the default location is /tmp/scp/\
221                        build-output')
222
223    parser.add_argument('-sc', '--skip-container', dest='skip_container',
224                        required=False, default=False, action='store_true',
225                        help='Skip container execution.')
226
227    return parser.parse_args()
228
229
230if __name__ == "__main__":
231    args = parse_args()
232    sys.exit(main(args.ignore_errors, args.skip_container, args.log_level,
233                  args.output_path))
234