# 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 click from platformio.platform.factory import PlatformFactory from platformio.unittest.exception import UnitTestSuiteError from platformio.unittest.result import TestCase, TestCaseSource, TestStatus from platformio.unittest.runners.mixins.embedded import TestRunnerEmbeddedMixin from platformio.unittest.runners.mixins.native import TestRunnerNativeMixin CTX_META_TEST_IS_RUNNING = __name__ + ".test_running" CTX_META_TEST_RUNNING_NAME = __name__ + ".test_running_name" class TestRunnerOptions: # pylint: disable=too-many-instance-attributes def __init__( # pylint: disable=too-many-arguments self, verbose=False, without_building=False, without_uploading=False, without_testing=False, without_debugging=True, upload_port=None, test_port=None, no_reset=False, monitor_rts=None, monitor_dtr=None, ): self.verbose = verbose self.without_building = without_building self.without_uploading = without_uploading self.without_testing = without_testing self.without_debugging = without_debugging self.upload_port = upload_port self.test_port = test_port self.no_reset = no_reset self.monitor_rts = monitor_rts self.monitor_dtr = monitor_dtr class TestRunnerBase(TestRunnerNativeMixin, TestRunnerEmbeddedMixin): NAME = None EXTRA_LIB_DEPS = None TESTCASE_PARSE_RE = None def __init__(self, test_suite, project_config, options=None): self.test_suite = test_suite self.options = options self.project_config = project_config self.platform = PlatformFactory.new( self.project_config.get(f"env:{self.test_suite.env_name}", "platform"), autoinstall=True, ) self.cmd_ctx = None @property def name(self): return self.__class__.__name__.replace("TestRunner", "").lower() def get_test_speed(self): return int( self.project_config.get(f"env:{self.test_suite.env_name}", "test_speed") ) def start(self, cmd_ctx): # setup command context self.cmd_ctx = cmd_ctx self.cmd_ctx.meta[CTX_META_TEST_IS_RUNNING] = True if self.test_suite.test_name != "*": self.cmd_ctx.meta[CTX_META_TEST_RUNNING_NAME] = self.test_suite.test_name self.test_suite.on_start() try: self.setup() for stage in ("build", "upload", "run"): getattr(self, f"stage_{stage}")() except Exception as exc: # pylint: disable=broad-except if str(exc) != "1": # from returncode click.secho(str(exc), fg="red", err=True) self.test_suite.on_error(exc) finally: self.test_suite.on_finish() self.teardown() def setup(self): pass def stage_build(self): if self.options.without_building: return None click.secho("Building...", bold=self.options.verbose) targets = ["__test"] if not self.options.without_debugging: targets.append("__debug") if self.platform.is_embedded(): targets.append("checkprogsize") return self.run_project_targets(targets) def stage_upload(self): if self.options.without_uploading or not self.platform.is_embedded(): return None click.secho("Uploading...", bold=self.options.verbose) targets = ["upload"] if self.options.without_building: targets.append("nobuild") else: targets.append("__test") if not self.options.without_debugging: targets.append("__debug") return self.run_project_targets(targets) def stage_run(self): if self.options.without_testing: return None click.secho("Running...", bold=self.options.verbose) if self.platform.is_embedded(): return self.stage_run_on_target() return self.stage_run_on_host() def teardown(self): pass def run_project_targets(self, targets): # pylint: disable=import-outside-toplevel from platformio.commands.run.command import cli as run_cmd assert self.cmd_ctx try: return self.cmd_ctx.invoke( run_cmd, project_conf=self.project_config.path, upload_port=self.options.upload_port, verbose=self.options.verbose, silent=not self.options.verbose, environment=[self.test_suite.env_name], disable_auto_clean="nobuild" in targets, target=targets, ) except Exception as exc: raise UnitTestSuiteError(exc) def configure_build_env(self, env): # pylint: disable=no-self-use """ Configure SCons build environment Called in "builder/tools/piotest" tool """ return env def on_run_output(self, data): click.echo(data, nl=False) self.parse_testcases(data) def parse_testcases(self, data): if not self.TESTCASE_PARSE_RE: raise NotImplementedError() for line in data.split("\n"): line = line.strip() if not line: continue match = self.TESTCASE_PARSE_RE.search(line) if not match: continue data = match.groupdict() source = None if "source_file" in data: source = TestCaseSource( file=data["source_file"], line=data.get("source_line") ) self.test_suite.add_case( TestCase( name=data.get("name"), status=TestStatus.from_string(data.get("status")), message=data.get("message"), stdout=line, source=source, ) )