tbot

Testcase Decorators

In tbot, testcases are marked using one of the following decorators. This will make tbot aware of the testcase and allows you to call it from the commandline. The testcase decorators will also track time and success of each run.

tbot.testcase(arg: str) AbstractContextManager[None][source]
tbot.testcase(arg: F_tc) F_tc

Mark a testcase.

This function can be used in two ways; either as a decorator for a function or as a context-manager. The first form is probably what is most commonly used because testcases defined using this decorator will be callable from the commandline.

Decorator:

@tbot.testcase
def foobar_testcase(x: str) -> int:
    return int(x, 16)

Context-Manager:

with tbot.testcase("test_foo_bar"):
    ...
tbot.named_testcase(name: str) Callable[[F_tc], F_tc][source]

Decorate a function to make it a testcase, but with a different name.

The testcase’s name is relevant for log-events and when calling it from the commandline.

Example:

@tbot.named_testcase("my_different_testcase")
def foobar_testcase(x: str) -> int:
    return int(x, 16)

(On the commandline you’ll have to run tbot my_different_testcase now.)

Context

The new mechanism for machine management is the context (superseding tbot.selectable). The global context is stored in tbot.ctx which is an instance of tbot.Context. Read the Context guide for a detailed introduction.

tbot.ctx: tbot.Context

The global context. This context should be used in testcases for accessing machines via the following pattern:

Single Machine:

@tbot.testcase
def test_with_labhost():
    with tbot.ctx.request(tbot.role.LabHost) as lh:
        lh.exec0("uname", "-a")

Multiple Machines:

@tbot.testcase
def test_with_board_and_lab():
   with tbot.ctx() as cx:
      lh = cx.request(tbot.role.LabHost)
      lnx = cx.request(tbot.role.BoardLinux)

      lh.exec0("hostname")
      lnx.exec0("hostname")

See the tbot.Context class below for the API details.

class tbot.Context(*, keep_alive: bool = False, add_defaults: bool = False, reset_on_error_by_default: bool = False)[source]

Bases: AbstractContextManager

A context which machines can be registered in and where instances can be retrieved from.

You will usually access the global context tbot.ctx which is an instance of this class instead of instantiating tbot.Context yourself. See the Context guide for a detailed introduction.

In case you do need to construct a context yourself, there are a few customization possibilities:

Parameters:
  • keep_alive (bool) –

    Whether machines should be immediately de-initialized once their context-manager is exited or whether they should be “kept alive” for a future request to immediately re-access them.

    Warning

    Keeping instances alive can have unintended side-effects: If, for example, a test brings a machine into an unusable state and then fails, a followup testcase could gain access to the same broken instance without reinitialization.

    To avoid such problems, always write testcase to leave the instance in a clean state. If a testcase can’t guarantee this, it should request the instance with reset_on_error=True or even exclusive=True.

  • add_defaults (bool) – Add default machines for some roles from tbot.role, for example for tbot.role.LocalHost. Defaults to False.

  • reset_on_error_by_default (bool) – Set reset_on_error=True for all request() s which do not explicitly overwrite it. It is a good idea to set this in conjunction with keep_alive=True.

register(machine: Type[M], roles: Any | List[Any], *, weak: bool = False) None[source]

Register a machine in this context for certain roles.

Registers the machine-class machine for the role roles or, if roles is a list, for all roles it contains. If for any role a machine is already registered, an exception is thrown.

This function is usually called in the register_machines() function of a lab- or board-config:

class SomeLabHostClass(...):
    ...

def register_machines(ctx: tbot.Context) -> None:
    ctx.register(SomeLabHostClass, tbot.role.LabHost)

    # or, to register for multiple roles
    ctx.register(SomeLabHostClass, [tbot.role.LabHost, tbot.role.BuildHost])
Parameters:
  • machine – A concrete machine-class to be registered. This machine-class will later be instantiated on request via its Connector.from_context() classmethod.

  • roles – Either a single role or a list of roles for which machine should be registered.

  • weak (bool) – Changes the way registration works: The machine is only registered for those roles which do not already have a machine registered. It will be registered as a weak default which means a later register will overwrite it without erroring. This is usually not necessary and should be used with care.

request(type: Callable[[...], M], *, reset: bool = False, exclusive: bool = False, reset_on_error: bool | None = None) Iterator[M][source]

Request a machine instance from this context.

Requests an instance of the role type from the context. If no instance exists, one will be created. If a previous testcase has already requested such an instance, the same instance is returned (this behavior can be controlled with the reset and exclusive keyword arguments).

This function must be used as a context manager:

@tbot.testcase
def foo_bar():
    with tbot.ctx.request(tbot.role.LabHost) as lh:
        lh.exec0("uname", "-a")

Alternatively, if you need multiple machines, a pattern similar to contextlib.ExitStack can be used:

@tbot.testcase
def foo_bar():
    with tbot.ctx() as cx:
        lh = cx.request(tbot.role.LabHost)
        bh = cx.request(tbot.role.BuildHost)

        lh.exec0("cat", "/etc/os-release")
        bh.exec0("free", "-h")

The semantics of a request() can be controlled further by the reset, exclusive, and reset_on_error keyword arguments. See their documentation for the details.

Parameters:
  • type (tbot.role.Role) – The role for which a machine instance is requested.

  • reset (bool) –

    Controls what happens if an instance already exists:

    • False (default): If an instance already exists due to a previous request(), it will be returned (both requests share it).

    • True: If an instance already exists due to a previous request(), it will be torn down and re-initialized (the previous request thus looses access to it).

    reset=True can, for example, be used to write a testcase where the DUT is powercycled:

    @tbot.testcase
    def test_with_reboot():
        with tbot.ctx.request(tbot.role.BoardUBoot) as ub:
            ub.exec0("version")
    
        # Device will be powercycled here, even though if some "outer"
        # context for U-Boot is still active.  Note that such an outer
        # context will loose access to the instance after this point.
    
        with tbot.ctx.request(tbot.role.BoardUBoot, reset=True) as ub:
            ub.exec0("version")
    

  • exclusive (bool) –

    Controls whether other requests can get access to the same instance while this request is active:

    • False (default): A request() after this one will get shared access to the same instance.

    • True: Any future request() while this one is active is forbidden and will fail. Once this request() ends, the instance will be torn down so future requests will need to re-initialize it.

    This mode should be used when you are going to do changes to the instance which could potentially bring it into a state that other testcases won’t expect.

  • reset_on_error (bool) –

    Controls behavior when the context-manager returned by this request() is exited abnormally via an exception:

    • False (default): The exception is ignored (and just propagates further up), no special behavior.

    • True: Instructs the Context to forcefully de-initialize this instance if the context-manager returned from this request() was exited with an exception. The exception is then of course propagated further up.

    This can be useful to ensure a follow-up request will always get a clean instance, even when something went wrong here. This is especially relevant for a Context which has keep_alive=True.

    If exclusive=True, reset_on_error=True is essentially a no-op.

    The default might be True if the Context was instantiated with reset_on_error_by_default=True.

get_machine_class(type: Callable[[...], M]) Type[M][source]

Return the registered machine class for a Role.

teardown_if_alive(type: Callable[[...], M]) bool[source]

Tear down any existing machine instances for a certain role.

This is useful, for example, when there might be a BoardLinux instance active and you need to get into BoardUBoot.

Returns:

Boolean whether an instance was alive and torn down (True) or whether no instance was alive (False).

New in version 0.9.3.

reconfigure(*, keep_alive: bool | None = None, reset_on_error_by_default: bool | None = None) Iterator[Context][source]

Temporarily reconfigure this context (e.g. keep_alive flag).

This method allows you to temporarily change flags for this context and have them restored afterwards. For example, this can be useful for running a test-suite with the keep_alive and reset_on_error flags enabled.

Example:

with ctx.reconfigure(keep_alive=True):
    ...

Once the reconfiguration context-manager exits, the old state will be restored. This especially means that any machines which were kept alive due to the reconfiguration (but have no active outside users) will be torn down before returning to the old state.

New in version 0.9.1.

is_active() bool[source]

Check whether this context was already “activated” by entering it.

For the tbot.Context to work properly, it should be entered as a context-manager at least once (but it is okay to do it multiple times):

with tbot.ctx:
    ...

is_active() can be used to check if this has already happened. This can be used as an indication whether the context was already initialized or not. If it wasn’t, you probably need to register machines for this context first (for example by loading configuration modules).

New in version 0.10.1.

Testcase Skipping

Sometimes a test can only run with certain prerequisites met. You can write a testcase to automatically skip when they aren’t, using tbot.skip():

tbot.skip(reason: str) NoReturn[source]

Skip this testcase.

A skipped testcase will return None instead of whatever type it would return normally. This might not make sense for certain testcases and might violate the type-annotation. Only use it, if it really makes sense!

Example:

@tbot.testcase
@tbot.with_lab
def test_something(lh) -> None:
    p = lh.fsroot / "dev" / "somedevice"

    if not p.is_char_device():
        tbot.skip("somedevice not present on this host")

    ...
class tbot.SkipException[source]

Bases: Exception

Exception to be used when a testcase is skipped.

Raising a SkipException will be caught in the tbot.testcase() decorator and the testcase will return None. This might violate the type-annotations so it should only be used if calling code can deal with a testcase returning None.

Deprecated items

Convenience Decorators

tbot.with_lab(tc: F_lh) Callable[[None, None, None], Any][source]

Warning

This decorator is deprecated! Use tbot.ctx instead:

@tbot.testcase
def testcase_with_lab() -> None:
    with tbot.ctx.request(tbot.role.LabHost) as lh:
        lh.exec0("uname", "-a")

Decorate a function to automatically supply the lab-host as an argument.

The idea is that when using this decorator and calling the testcase without a lab-host, tbot will automatically acquire the default lab.

Example:

from tbot.machine import linux

@tbot.testcase
@tbot.with_lab
def testcase_with_lab(lh: linux.Lab) -> None:
    lh.exec0("uname", "-a")

This is essentially syntactic sugar for:

import typing
import tbot
from tbot.machine import linux

@tbot.testcase
def testcase_with_lab(
    lab: typing.Optional[linux.Lab] = None,
) -> None:
    with lab or tbot.acquire_lab() as lh:
        lh.exec0("uname", "-a")

Warning

While making your life a lot easier, this decorator unfortunately has a drawback: It will erase the type signature of your testcase, so you can no longer rely on type-checking when using the testcase downstream.

Changed in version 0.10.0: This decorator is now officially deprecated in favor of the Context mechanism.

tbot.with_uboot(tc: F_ub) Callable[[None, None, None], Any][source]

Warning

This decorator is deprecated! Use tbot.ctx instead:

@tbot.testcase
def testcase_with_uboot() -> None:
    with tbot.ctx.request(tbot.role.BoardUBoot) as ub:
        ub.exec0("version")

Decorate a function to automatically supply a U-Boot machine as an argument.

The idea is that when using this decorator and calling the testcase without an already initialized U-Boot machine, tbot will automatically acquire the selected one.

Example:

from tbot.machine import board

@tbot.testcase
@tbot.with_uboot
def testcase_with_uboot(ub: board.UBootShell) -> None:
    ub.exec0("version")

This is essentially syntactic sugar for:

import contextlib
import typing
import tbot
from tbot.machine import board, linux

@tbot.testcase
def testcase_with_uboot(
    lab_or_ub: typing.Union[linux.Lab, board.UBootShell, None] = None,
) -> None:
    with contextlib.ExitStack() as cx:
        lh: linux.Lab
        ub: board.UBootShell

        if isinstance(lab_or_ub, linux.Lab):
            lh = cx.enter_context(lab_or_ub)
        elif isinstance(lab_or_ub, board.UBootShell):
            lh = cx.enter_context(lab_or_ub.host)
        else:
            lh = cx.enter_context(tbot.acquire_lab())

        if isinstance(lab_or_ub, board.UBootShell):
            ub = cx.enter_context(lab_or_ub)
        else:
            b = cx.enter_context(tbot.acquire_board(lh))
            ub = cx.enter_context(tbot.acquire_uboot(b))

        ub.exec0("version")

Warning

While making your life a lot easier, this decorator unfortunately has a drawback: It will erase the type signature of your testcase, so you can no longer rely on type-checking when using the testcase downstream.

Changed in version 0.10.0: This decorator is now officially deprecated in favor of the Context mechanism.

tbot.with_linux(tc: F_lnx) Callable[[None, None, None], Any][source]

Warning

This decorator is deprecated! Use tbot.ctx instead:

@tbot.testcase
def testcase_with_linux() -> None:
    with tbot.ctx.request(tbot.role.BoardLinux) as lnx:
        lnx.exec0("uname", "-a")

Decorate a function to automatically supply a board Linux machine as an argument.

The idea is that when using this decorator and calling the testcase without an already initialized Linux machine, tbot will automatically acquire the selected one.

Example:

from tbot.machine import linux

@tbot.testcase
@tbot.with_linux
def testcase_with_linux(lnx: linux.LinuxShell) -> None:
    lnx.exec0("uname", "-a")

Warning

While making your life a lot easier, this decorator unfortunately has a drawback: It will erase the type signature of your testcase, so you can no longer rely on type-checking when using the testcase downstream.

Changed in version 0.10.0: This decorator is now officially deprecated in favor of the Context mechanism.

Default Machine Access

tbot.acquire_lab() LocalLabHost[source]

Warning

This function is deprecated! Use tbot.ctx instead:

@tbot.testcase
def testcase_with_lab() -> None:
    with tbot.ctx.request(tbot.role.LabHost) as lh:
        lh.exec0("uname", "-a")

Acquire a new connection to the LabHost.

If your lab-host is using a ParamikoConnector this will create a new ssh connection.

You should call this function as little as possible, because it can be very slow. If possible, try to reuse the labhost. A recipe for doing so is

import typing
import tbot
from tbot.machine import linux

@tbot.testcase
def my_testcase(
    lab: typing.Optional[linux.LinuxShell] = None,
) -> None:
    with lab or tbot.acquire_lab() as lh:
        # Your code goes here
        ...
Return type:

tbot.selectable.LabHost

Changed in version 0.10.0: This function is now officially deprecated in favor of the Context mechanism.

tbot.acquire_local() LocalLabHost[source]

Warning

This function is deprecated! Use tbot.ctx instead:

@tbot.testcase
def testcase_with_local() -> None:
    with tbot.ctx.request(tbot.role.LocalHost) as lo:
        lo.exec0("uname", "-a")

Acquire a machine for the local host.

Localhost machines are very cheap so they do not need to be shared like the others and you can create as many as you want. One usecase might be copying test-results to you local machine after the run.

Example:

import tbot

@tbot.testcase
def my_testcase() -> None:
    with tbot.acquire_local() as lo:
        lo.exec0("id", "-un")
        # On local machines you can access tbot's working directory:
        tbot.log.message(f"CWD: {lo.workdir}")

Changed in version 0.10.0: This function is now officially deprecated in favor of the Context mechanism.

tbot.acquire_board(lh: LocalLabHost) Board[source]

Warning

This function is deprecated! Use tbot.ctx instead:

@tbot.testcase
def testcase_with_board() -> None:
    with tbot.ctx.request(tbot.role.Board) as b:
        b.interactive()

Acquire the selected board.

If configured properly, tbot.acquire_board() will power on the hardware and open a serial-console for the selected board. Just by itself, this is not too useful, so you will usually follow it up immediately with a call to either tbot.acquire_uboot() or tbot.acquire_linux().

Example:

with tbot.acquire_lab() as lh:
    lh.exec0("echo", "Foo")
    with tbot.acquire_board(lh) as b, tbot.acquire_uboot(b) as ub:
        ub.exec0("version")

Changed in version 0.10.0: This function is now officially deprecated in favor of the Context mechanism.

tbot.acquire_uboot(board: Board, *args: Any) UBootMachine[source]

Warning

This function is deprecated! Use tbot.ctx instead:

@tbot.testcase
def testcase_with_uboot() -> None:
    with tbot.ctx.request(tbot.role.BoardUBoot) as ub:
        ub.exec0("version")

Acquire the selected board’s U-Boot shell.

As there can only be one instance of the selected board’s UBootShell at a time, your testcases should optionally take the UBootShell as a parameter. The recipe looks like this:

import contextlib
import typing
import tbot
from tbot.machine import board


@tbot.testcase
def my_testcase(
    lab: typing.Optional[tbot.selectable.LabHost] = None,
    uboot: typing.Optional[board.UBootShell] = None,
) -> None:
    with contextlib.ExitStack() as cx:
        lh = cx.enter_context(lab or tbot.acquire_lab())
        if uboot is not None:
            ub = uboot
        else:
            b = cx.enter_context(tbot.acquire_board(lh))
            ub = cx.enter_context(tbot.acquire_uboot(b))

        ...
Return type:

tbot.selectable.UBootMachine

Changed in version 0.10.0: This function is now officially deprecated in favor of the Context mechanism.

tbot.acquire_linux(b: Board | UBootMachine, *args: Any) LinuxMachine[source]

Warning

This function is deprecated! Use tbot.ctx instead:

@tbot.testcase
def testcase_with_linux() -> None:
    with tbot.ctx.request(tbot.role.BoardLinux) as lnx:
        lnx.exec0("cat", "/etc/os-release")

Acquire the board’s Linux shell.

Can either boot from a previously created U-Boot (if the implementation supports this) or directly.

To write testcases that work both from the commandline and when called from other testcases, use the following recipe:

import contextlib
import typing
import tbot
from tbot.machine import board


@tbot.testcase
def test_testcase(
    lab: typing.Optional[tbot.selectable.LabHost] = None,
    board_linux: typing.Optional[board.LinuxMachine] = None,
) -> None:
    with contextlib.ExitStack() as cx:
        lh = cx.enter_context(lab or tbot.acquire_lab())
        if board_linux is not None:
            lnx = board_linux
        else:
            b = cx.enter_context(tbot.acquire_board(lh))
            lnx = cx.enter_context(tbot.acquire_linux(b))

        ...
Return type:

tbot.machine.linux.LinuxShell

Changed in version 0.10.0: This function is now officially deprecated in favor of the Context mechanism.