1# vim: set syntax=python ts=4 :
2#
3# Copyright (c) 2022 Google
4# SPDX-License-Identifier: Apache-2.0
5
6import colorama
7import logging
8import os
9import shutil
10import sys
11import time
12
13from colorama import Fore
14
15from twisterlib.testplan import TestPlan
16from twisterlib.reports import Reporting
17from twisterlib.hardwaremap import HardwareMap
18from twisterlib.coverage import run_coverage
19from twisterlib.runner import TwisterRunner
20from twisterlib.environment import TwisterEnv
21from twisterlib.package import Artifacts
22
23logger = logging.getLogger("twister")
24logger.setLevel(logging.DEBUG)
25
26
27def setup_logging(outdir, log_file, verbose, timestamps):
28    # create file handler which logs even debug messages
29    if log_file:
30        fh = logging.FileHandler(log_file)
31    else:
32        fh = logging.FileHandler(os.path.join(outdir, "twister.log"))
33
34    fh.setLevel(logging.DEBUG)
35
36    # create console handler with a higher log level
37    ch = logging.StreamHandler()
38
39    if verbose > 1:
40        ch.setLevel(logging.DEBUG)
41    else:
42        ch.setLevel(logging.INFO)
43
44    # create formatter and add it to the handlers
45    if timestamps:
46        formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
47    else:
48        formatter = logging.Formatter("%(levelname)-7s - %(message)s")
49
50    formatter_file = logging.Formatter(
51        "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
52    )
53    ch.setFormatter(formatter)
54    fh.setFormatter(formatter_file)
55
56    # add the handlers to logger
57    logger.addHandler(ch)
58    logger.addHandler(fh)
59
60
61def init_color(colorama_strip):
62    colorama.init(strip=colorama_strip)
63
64
65def main(options, default_options):
66    start_time = time.time()
67
68    # Configure color output
69    color_strip = False if options.force_color else None
70
71    colorama.init(strip=color_strip)
72    init_color(colorama_strip=color_strip)
73
74    previous_results = None
75    # Cleanup
76    if options.no_clean or options.only_failed or options.test_only or options.report_summary is not None:
77        if os.path.exists(options.outdir):
78            print("Keeping artifacts untouched")
79    elif options.last_metrics:
80        ls = os.path.join(options.outdir, "twister.json")
81        if os.path.exists(ls):
82            with open(ls, "r") as fp:
83                previous_results = fp.read()
84        else:
85            sys.exit(f"Can't compare metrics with non existing file {ls}")
86    elif os.path.exists(options.outdir):
87        if options.clobber_output:
88            print("Deleting output directory {}".format(options.outdir))
89            shutil.rmtree(options.outdir)
90        else:
91            for i in range(1, 100):
92                new_out = options.outdir + ".{}".format(i)
93                if not os.path.exists(new_out):
94                    print("Renaming output directory to {}".format(new_out))
95                    shutil.move(options.outdir, new_out)
96                    break
97            else:
98                sys.exit(f"Too many '{options.outdir}.*' directories. Run either with --no-clean, "
99                         "or --clobber-output, or delete these directories manually.")
100
101    previous_results_file = None
102    os.makedirs(options.outdir, exist_ok=True)
103    if options.last_metrics and previous_results:
104        previous_results_file = os.path.join(options.outdir, "baseline.json")
105        with open(previous_results_file, "w") as fp:
106            fp.write(previous_results)
107
108    VERBOSE = options.verbose
109    setup_logging(options.outdir, options.log_file, VERBOSE, options.timestamps)
110
111    env = TwisterEnv(options, default_options)
112    env.discover()
113
114    hwm = HardwareMap(env)
115    ret = hwm.discover()
116    if ret == 0:
117        return 0
118
119    env.hwm = hwm
120
121    tplan = TestPlan(env)
122    try:
123        tplan.discover()
124    except RuntimeError as e:
125        logger.error(f"{e}")
126        return 1
127
128    if tplan.report() == 0:
129        return 0
130
131    try:
132        tplan.load()
133    except RuntimeError as e:
134        logger.error(f"{e}")
135        return 1
136
137    if VERBOSE > 1:
138        # if we are using command line platform filter, no need to list every
139        # other platform as excluded, we know that already.
140        # Show only the discards that apply to the selected platforms on the
141        # command line
142
143        for i in tplan.instances.values():
144            if i.status == "filtered":
145                if options.platform and i.platform.name not in options.platform:
146                    continue
147                logger.debug(
148                    "{:<25} {:<50} {}SKIPPED{}: {}".format(
149                        i.platform.name,
150                        i.testsuite.name,
151                        Fore.YELLOW,
152                        Fore.RESET,
153                        i.reason,
154                    )
155                )
156
157    report = Reporting(tplan, env)
158    plan_file = os.path.join(options.outdir, "testplan.json")
159    if not os.path.exists(plan_file):
160        report.json_report(plan_file)
161
162    if options.save_tests:
163        report.json_report(options.save_tests)
164        return 0
165
166    if options.report_summary is not None:
167        if options.report_summary < 0:
168            logger.error("The report summary value cannot be less than 0")
169            return 1
170        report.synopsis()
171        return 0
172
173    if options.device_testing and not options.build_only:
174        print("\nDevice testing on:")
175        hwm.dump(filtered=tplan.selected_platforms)
176        print("")
177
178    if options.dry_run:
179        duration = time.time() - start_time
180        logger.info("Completed in %d seconds" % (duration))
181        return 0
182
183    if options.short_build_path:
184        tplan.create_build_dir_links()
185
186    runner = TwisterRunner(tplan.instances, tplan.testsuites, env)
187    runner.duts = hwm.duts
188    runner.run()
189
190    # figure out which report to use for size comparison
191    report_to_use = None
192    if options.compare_report:
193        report_to_use = options.compare_report
194    elif options.last_metrics:
195        report_to_use = previous_results_file
196
197    report.footprint_reports(
198        report_to_use,
199        options.show_footprint,
200        options.all_deltas,
201        options.footprint_threshold,
202        options.last_metrics,
203    )
204
205    duration = time.time() - start_time
206
207    if VERBOSE > 1:
208        runner.results.summary()
209
210    report.summary(runner.results, options.disable_unrecognized_section_test, duration)
211
212    coverage_completed = True
213    if options.coverage:
214        if not options.build_only:
215            coverage_completed = run_coverage(tplan, options)
216        else:
217            logger.info("Skipping coverage report generation due to --build-only.")
218
219    if options.device_testing and not options.build_only:
220        hwm.summary(tplan.selected_platforms)
221
222    report.save_reports(
223        options.report_name,
224        options.report_suffix,
225        options.report_dir,
226        options.no_update,
227        options.platform_reports,
228    )
229
230    report.synopsis()
231
232    if options.package_artifacts:
233        artifacts = Artifacts(env)
234        artifacts.package()
235
236    logger.info("Run completed")
237    if (
238        runner.results.failed
239        or runner.results.error
240        or (tplan.warnings and options.warnings_as_errors)
241        or (options.coverage and not coverage_completed)
242    ):
243        return 1
244
245    return 0
246