# Copyright (c) 2014-present PlatformIO # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import fnmatch import os import shutil import click from platformio import app, exception, fs, util from platformio.project.config import ProjectConfig from platformio.unittest.exception import TestDirNotExistsError from platformio.unittest.reports.base import TestReportFactory from platformio.unittest.result import TestStatus, TestSuite, TestSummary from platformio.unittest.runners.base import TestRunnerOptions from platformio.unittest.runners.factory import TestRunnerFactory @click.command("test", short_help="Unit Testing") @click.option("--environment", "-e", multiple=True, metavar="") @click.option( "--filter", "-f", multiple=True, metavar="", help="Filter tests by a pattern", ) @click.option( "--ignore", "-i", multiple=True, metavar="", help="Ignore tests by a pattern", ) @click.option("--upload-port") @click.option("--test-port") @click.option( "-d", "--project-dir", default=os.getcwd, type=click.Path( exists=True, file_okay=False, dir_okay=True, writable=True, resolve_path=True ), ) @click.option( "-c", "--project-conf", type=click.Path( exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True ), ) @click.option("--without-building", is_flag=True) @click.option("--without-uploading", is_flag=True) @click.option("--without-testing", is_flag=True) @click.option("--no-reset", is_flag=True) @click.option( "--monitor-rts", default=None, type=click.IntRange(0, 1), help="Set initial RTS line state for Serial Monitor", ) @click.option( "--monitor-dtr", default=None, type=click.IntRange(0, 1), help="Set initial DTR line state for Serial Monitor", ) @click.option("--output-format", type=click.Choice(["json", "junit"])) @click.option( "--output-path", default=os.getcwd, type=click.Path(dir_okay=True, resolve_path=True), ) @click.option("--verbose", "-v", is_flag=True) @click.pass_context def unittest_cmd( # pylint: disable=too-many-arguments,too-many-locals,redefined-builtin ctx, environment, ignore, filter, upload_port, test_port, project_dir, project_conf, without_building, without_uploading, without_testing, no_reset, monitor_rts, monitor_dtr, output_format, output_path, verbose, ): app.set_session_var("custom_project_conf", project_conf) with fs.cd(project_dir): config = ProjectConfig.get_instance(project_conf) config.validate(envs=environment) test_names = get_test_names(config) if not verbose: click.echo("Verbose mode can be enabled via `-v, --verbose` option") click.secho("Collected %d tests" % len(test_names), bold=True, nl=False) click.echo(" (%s)" % ", ".join(test_names)) test_summary = TestSummary(os.path.basename(project_dir)) default_envs = config.default_envs() for env_name in config.envs(): for test_name in test_names: test_suite = TestSuite(env_name, test_name) test_summary.add_suite(test_suite) # filter and ignore patterns patterns = dict(filter=list(filter), ignore=list(ignore)) for key in patterns: if patterns[key]: # overriden from CLI continue patterns[key].extend( config.get(f"env:{env_name}", f"test_{key}", []) ) skip_conditions = [ environment and env_name not in environment, not environment and default_envs and env_name not in default_envs, test_name != "*" and patterns["filter"] and not any( fnmatch.fnmatch(test_name, p) for p in patterns["filter"] ), test_name != "*" and any(fnmatch.fnmatch(test_name, p) for p in patterns["ignore"]), ] if any(skip_conditions): continue runner = TestRunnerFactory.new( test_suite, config, TestRunnerOptions( verbose=verbose, without_building=without_building, without_uploading=without_uploading, without_testing=without_testing, upload_port=upload_port, test_port=test_port, no_reset=no_reset, monitor_rts=monitor_rts, monitor_dtr=monitor_dtr, ), ) click.echo() print_suite_header(test_suite) runner.start(ctx) print_suite_footer(test_suite) # automatically generate JSON report for PIO IDE TestReportFactory.new("json", test_summary).generate( os.path.join( config.get("platformio", "build_dir"), "pio-test-report-latest.json" ) ) # Reset custom project config app.set_session_var("custom_project_conf", None) stdout_report = TestReportFactory.new("stdout", test_summary) stdout_report.generate(verbose=verbose) if output_format: custom_report = TestReportFactory.new(output_format, test_summary) custom_report.generate(output_path=output_path, verbose=True) if test_summary.is_errored or test_summary.get_status_nums(TestStatus.FAILED): raise exception.ReturnErrorCode(1) def get_test_names(config): test_dir = config.get("platformio", "test_dir") if not os.path.isdir(test_dir): raise TestDirNotExistsError(test_dir) names = [] for root, _, __ in os.walk(test_dir): if not os.path.basename(root).startswith("test_"): continue names.append(os.path.relpath(root, test_dir)) if not names: names = ["*"] return names def print_suite_header(test_suite): click.echo( "Processing %s in %s environment" % ( click.style(test_suite.test_name, fg="yellow", bold=True), click.style(test_suite.env_name, fg="cyan", bold=True), ) ) terminal_width, _ = shutil.get_terminal_size() click.secho("-" * terminal_width, bold=True) def print_suite_footer(test_suite): is_error = test_suite.status in (TestStatus.FAILED, TestStatus.ERRORED) util.print_labeled_bar( "%s [%s] Took %.2f seconds" % ( click.style( "%s:%s" % (test_suite.env_name, test_suite.test_name), bold=True ), ( click.style(test_suite.status.name, fg="red", bold=True) if is_error else click.style("PASSED", fg="green", bold=True) ), test_suite.duration, ), is_error=is_error, sep="-", )