Source code for tbot

"""
TBot
----
"""
import time
import typing
import traceback
import enforce

from tbot import log  # noqa: F401
from tbot import log_events  # noqa: F401
from tbot import tc  # noqa: F401
import tbot.machine
import tbot.config

from tbot.testcase_collector import testcase  # noqa: F401


class TestcaseFailure(Exception):
    """
    A testcase detected an error condition

    Raise this exception if an error occured, that
    was manually detected (don't raise it in an except block,
    use the original exception there)
    """
    pass


class InvalidUsageException(Exception):
    """
    A testcase/utility was invoked with incorrect arguments

    Raise this exception if your testcase was called with incorrect
    parameters (=programmer error). Preferably ensure correct calling
    by using type annotations.
    """
    pass


# pylint: disable=too-many-instance-attributes
[docs]class TBot: """ Main class of TBot, you usually do not need to instanciate this yourself :param tbot.config.Config config: A configuration to be used :param dict testcases: Testcases available to this instance :param bool new: Whether this is a new instance that should create a noenv machine. Always ``True`` unless you know what you are doing. :ivar config: :class:`tbot.config.Config()` :ivar testcases: All available testcases :ivar machines: All available machines :class:`tbot.machine.machine.MachineManager()` """ def __init__( self, config: tbot.config.Config, testcases: dict, new: bool = True, interactive: bool = False, ) -> None: self.config = config self.testcases = testcases self.layer = 0 self.interactive = interactive self._old_inst: typing.Optional[TBot] = None self.destruct_machines: typing.List[tbot.machine.Machine] = list() if new: self.machines = tbot.machine.MachineManager(self) labhost = tbot.machine.MachineLabNoEnv() labhost._setup(self) # pylint: disable=protected-access self.machines[labhost.common_machine_name] = labhost self.machines[labhost.unique_machine_name] = labhost self.destruct_machines.append(labhost) @property def shell(self) -> tbot.machine.Machine: """ The default host machine """ return self.machines["host"] @property def boardshell(self) -> tbot.machine.MachineBoard: """ The default board machine """ boardmachine = self.machines["board"] if not isinstance(boardmachine, tbot.machine.MachineBoard): raise InvalidUsageException("BoardMachine is not a 'MachineBoard'") return boardmachine
[docs] def call_then( self, tcs: typing.Union[str, typing.Callable], **kwargs: typing.Any ) -> typing.Callable: """ Decorator to call a testcase with a function as a payload ("and_then" argument) :param tcs: The testcase to call :type tcs: str or typing.Callable :param dict kwargs: Additional arguments for the testcase :returns: The decorated function :rtype: typing.Callable """ def _decorator(f: typing.Callable) -> typing.Any: kwargs["and_then"] = f self.call(tcs, **kwargs) return f return _decorator
[docs] def call( self, tcs: typing.Union[str, typing.Callable], *, fail_ok: bool = False, doc: bool = True, **kwargs: typing.Any, ) -> typing.Any: """ Call a testcase :param tcs: The testcase to be called. Can either be a string or a callable :type tcs: str or typing.Callable :param bool fail_ok: Whether a failure in this testcase is tolerable :param bool doc: Whether documentation should be generated in this testcase :param dict kwargs: Additional arguments for the testcase :returns: The return value from the testcase """ name = tcs if isinstance(tcs, str) else f"@{tcs.__name__}" tbot.log_events.testcase_begin(name) self.layer += 1 tbot.log.set_layer(self.layer) previous_doc = tbot.log.LOG_DO_DOC tbot.log.LOG_DO_DOC = previous_doc and doc start_time = time.monotonic() try: if isinstance(tcs, str): retval = self.testcases[tcs](self, **kwargs) else: retval = enforce.runtime_validation(tcs)(self, **kwargs) except Exception as e: # pylint: disable=broad-except # Cleanup is done by "with" handler __exit__ # A small hack to ensure, the exception is only added once: if "__tbot_exc_catched" not in e.__dict__: exc_name = type(e).__module__ + "." + type(e).__qualname__ tbot.log_events.exception(exc_name, traceback.format_exc()) e.__dict__["__tbot_exc_catched"] = True self.layer -= 1 run_duration = time.monotonic() - start_time tbot.log_events.testcase_end(name, run_duration, False, fail_ok) tbot.log.set_layer(self.layer) tbot.log.LOG_DO_DOC = previous_doc raise self.layer -= 1 run_duration = time.monotonic() - start_time tbot.log_events.testcase_end(name, run_duration, True) tbot.log.set_layer(self.layer) tbot.log.LOG_DO_DOC = previous_doc return retval
[docs] def machine(self, mach: tbot.machine.Machine) -> "TBot": """ Create a new TBot instance with a new machine :param tbot.machine.machine.Machine mach: The machine to be added in the new instance :returns: The new TBot instance, which has to be used inside a with statement :rtype: TBot """ new_inst = TBot( self.config, self.testcases, False, interactive=self.interactive ) new_inst.layer = self.layer new_inst.machines = tbot.machine.MachineManager( new_inst, self.machines.connection ) for machine_name in self.machines.keys(): new_inst.machines[machine_name] = self.machines[machine_name] old_mach = ( new_inst.machines[mach.common_machine_name] if mach.common_machine_name in new_inst.machines else None ) new_mach = mach._setup(new_inst, old_mach) # pylint: disable=protected-access new_inst.machines[mach.common_machine_name] = new_mach new_inst.machines[mach.unique_machine_name] = new_mach if new_mach is not old_mach: new_inst.destruct_machines.append(new_mach) new_inst._old_inst = self return new_inst
[docs] def with_board_uboot(self) -> "TBot": """ Shortcut to create a new TBot instance with a U-Boot boardmachine :returns: The new TBot instance, which has to be used inside a with statement :rtype: TBot """ return self.machine(tbot.machine.MachineBoardUBoot())
[docs] def with_board_linux(self) -> "TBot": """ Shortcut to create a new TBot instance with a Linux boardmachine :returns: The new TBot instance, which has to be used inside a with statement :rtype: TBot """ return self.machine(tbot.machine.MachineBoardLinux())
def __enter__(self) -> "TBot": return self
[docs] def destruct(self) -> None: """ Destruct this TBot instance and all associated machines. This method will be called automatically when exiting a with statement. """ # Make sure logfile is written tbot.log.flush_log() # Destruct all machines that need to be destructed for mach in self.destruct_machines: # pylint: disable=protected-access mach._destruct(self) # Make sure, we don't destruct twice self.destruct_machines = []
def __exit__( self, exc_type: typing.Any, exc_value: typing.Any, trceback: typing.Any ) -> None: self.destruct() # Hack to make this TBot behave like it's parent after the end of a with # statement if self._old_inst is not None: self.machines = self._old_inst.machines self.destruct_machines = self._old_inst.destruct_machines