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 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 options.verbose > 1:
108        # if we are using command line platform filter, no need to list every
109        # other platform as excluded, we know that already.
110        # Show only the discards that apply to the selected platforms on the
111        # command line
112
113        for i in tplan.instances.values():
114            if i.status == TwisterStatus.FILTER:
115                if options.platform and not tplan.check_platform(i.platform, options.platform):
116                    continue
117                logger.debug(
118                    f"{i.platform.name:<25} {i.testsuite.name:<50}"
119                    f" {Fore.YELLOW}FILTERED{Fore.RESET}: {i.reason}"
120                )
121
122    report = Reporting(tplan, env)
123    plan_file = os.path.join(options.outdir, "testplan.json")
124    if not os.path.exists(plan_file):
125        report.json_report(plan_file, env.version)
126
127    if options.save_tests:
128        report.json_report(options.save_tests, env.version)
129        return 0
130
131    if options.report_summary is not None:
132        if options.report_summary < 0:
133            logger.error("The report summary value cannot be less than 0")
134            return 1
135        report.synopsis()
136        return 0
137
138    if options.device_testing and not options.build_only:
139        print("\nDevice testing on:")
140        hwm.dump(filtered=tplan.selected_platforms)
141        print("")
142
143    if options.dry_run:
144        duration = time.time() - start_time
145        logger.info(f"Completed in {duration} seconds")
146        return 0
147
148    if options.short_build_path:
149        tplan.create_build_dir_links()
150
151    runner = TwisterRunner(tplan.instances, tplan.testsuites, env)
152    # FIXME: This is a workaround for the fact that the hardware map can be usng
153    # the short name of the platform, while the testplan is using the full name.
154    #
155    # convert platform names coming from the hardware map to the full target
156    # name.
157    # this is needed to match the platform names in the testplan.
158    for d in hwm.duts:
159        if d.platform in tplan.platform_names:
160            d.platform = tplan.get_platform(d.platform).name
161    runner.duts = hwm.duts
162    runner.run()
163
164    # figure out which report to use for size comparison
165    report_to_use = None
166    if options.compare_report:
167        report_to_use = options.compare_report
168    elif options.last_metrics:
169        report_to_use = previous_results_file
170
171    report.footprint_reports(
172        report_to_use,
173        options.show_footprint,
174        options.all_deltas,
175        options.footprint_threshold,
176        options.last_metrics,
177    )
178
179    duration = time.time() - start_time
180
181    if options.verbose > 1:
182        runner.results.summary()
183
184    report.summary(runner.results, options.disable_unrecognized_section_test, duration)
185
186    report.coverage_status = True
187    if options.coverage and not options.disable_coverage_aggregation:
188        if not options.build_only:
189            report.coverage_status, report.coverage = run_coverage(options, tplan)
190        else:
191            logger.info("Skipping coverage report generation due to --build-only.")
192
193    if options.device_testing and not options.build_only:
194        hwm.summary(tplan.selected_platforms)
195
196    report.save_reports(
197        options.report_name,
198        options.report_suffix,
199        options.report_dir,
200        options.no_update,
201        options.platform_reports,
202    )
203
204    report.synopsis()
205
206    if options.package_artifacts:
207        artifacts = Artifacts(env)
208        artifacts.package()
209
210    if (
211        runner.results.failed
212        or runner.results.error
213        or (tplan.warnings and options.warnings_as_errors)
214        or (options.coverage and not report.coverage_status)
215    ):
216        if env.options.quit_on_failure:
217            logger.info("twister aborted because of a failure/error")
218        else:
219            logger.info("Run completed")
220        return 1
221
222    logger.info("Run completed")
223    return 0
224
225
226def main(options: argparse.Namespace, default_options: argparse.Namespace):
227    try:
228        return_code = twister(options, default_options)
229    finally:
230        close_logging()
231    return return_code
232