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