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