# tbot, Embedded Automation Tool
# Copyright (C) 2019 Harald Seiler
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import contextlib
import functools
import typing
import tbot
from tbot import selectable
from tbot.machine import linux, board
if typing.TYPE_CHECKING:
import mypy_extensions as mypy
else:
class mypy:
class KwArg:
def __new__(cls, ty: typing.Any) -> None:
pass
class VarArg:
def __new__(cls, ty: typing.Any) -> None:
pass
class DefaultArg:
def __new__(cls, ty: typing.Any, name: typing.Optional[str] = None) -> None:
pass
__all__ = ("testcase", "with_lab", "with_uboot", "with_linux")
F_tc = typing.TypeVar("F_tc", bound=typing.Callable[..., typing.Any])
def testcase(tc: F_tc) -> F_tc:
"""
Decorate a function to make it a testcase.
**Example**::
@tbot.testcase
def foobar_testcase(x: str) -> int:
return int(x, 16)
"""
@functools.wraps(tc)
def wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
with tbot.testcase(tc.__name__):
return tc(*args, **kwargs)
# This line will only be reached when a testcase was skipped.
# Return `None` as the placeholder return value.
return None
setattr(wrapped, "_tbot_testcase", tc.__name__)
return typing.cast(F_tc, wrapped)
[docs]def named_testcase(name: str) -> typing.Callable[[F_tc], F_tc]:
"""
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.)
"""
def _named_testcase(tc: F_tc) -> F_tc:
@functools.wraps(tc)
def wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
with tbot.testcase(name):
return tc(*args, **kwargs)
# This line will only be reached when a testcase was skipped.
# Return `None` as the placeholder return value.
return None
setattr(wrapped, "_tbot_testcase", name)
return typing.cast(F_tc, wrapped)
return _named_testcase
F_lh = typing.TypeVar("F_lh", bound=typing.Callable[..., typing.Any])
F_lab = typing.Callable[
[
mypy.DefaultArg(typing.Optional[linux.Lab], "lab"), # noqa: F821
mypy.VarArg(typing.Any),
mypy.KwArg(typing.Any),
],
typing.Any,
]
[docs]def with_lab(tc: F_lh) -> F_lab:
"""
.. warning::
This decorator is deprecated! Use :py:data:`tbot.ctx` instead:
.. code-block:: python
@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.
.. versionchanged:: 0.10.0
This decorator is now officially deprecated in favor of the
:ref:`context` mechanism.
"""
@functools.wraps(tc)
def wrapped(
lab: typing.Optional[linux.Lab] = None, *args: typing.Any, **kwargs: typing.Any
) -> typing.Any:
if lab is not None and not isinstance(lab, linux.Lab):
raise TypeError(f"Argument to {tc!r} must be a lab-host (found {lab!r})")
with lab or selectable.acquire_lab() as lh:
return tc(lh, *args, **kwargs)
# Adjust annotation
argname = tc.__code__.co_varnames[0]
wrapped.__annotations__[argname] = typing.Optional[linux.Lab]
return typing.cast(F_lab, wrapped)
F_ub = typing.TypeVar("F_ub", bound=typing.Callable[..., typing.Any])
F_uboot = typing.Callable[
[
mypy.DefaultArg(typing.Union[selectable.LabHost, board.UBootShell, None]),
mypy.VarArg(typing.Any),
mypy.KwArg(typing.Any),
],
typing.Any,
]
[docs]def with_uboot(tc: F_ub) -> F_uboot:
"""
.. warning::
This decorator is deprecated! Use :py:data:`tbot.ctx` instead:
.. code-block:: python
@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.
.. versionchanged:: 0.10.0
This decorator is now officially deprecated in favor of the
:ref:`context` mechanism.
"""
@functools.wraps(tc)
def wrapped(
arg: typing.Union[selectable.LabHost, board.UBootShell, None] = None,
*args: typing.Any,
**kwargs: typing.Any,
) -> typing.Any:
with contextlib.ExitStack() as cx:
lh: selectable.LabHost
ub: board.UBootShell
# Acquire LabHost
if arg is None:
lh = cx.enter_context(selectable.acquire_lab())
elif isinstance(arg, linux.Lab):
lh = cx.enter_context(arg)
elif not isinstance(arg, board.UBootShell):
raise TypeError(
f"Argument to {tc!r} must either be a lab-host or a UBootShell (found {arg!r})"
)
# Acquire U-Boot
if isinstance(arg, board.UBootShell):
ub = cx.enter_context(arg)
else:
b = cx.enter_context(selectable.acquire_board(lh))
ub = cx.enter_context(selectable.acquire_uboot(b))
return tc(ub, *args, **kwargs)
# Adjust annotation
argname = tc.__code__.co_varnames[0]
wrapped.__annotations__[argname] = typing.Union[
selectable.LabHost, board.UBootShell, None
]
return typing.cast(F_uboot, wrapped)
F_lnx = typing.TypeVar("F_lnx", bound=typing.Callable[..., typing.Any])
F_linux = typing.Callable[
[
mypy.DefaultArg(typing.Union[selectable.LabHost, linux.LinuxShell, None]),
mypy.VarArg(typing.Any),
mypy.KwArg(typing.Any),
],
typing.Any,
]
[docs]def with_linux(tc: F_lnx) -> F_linux:
"""
.. warning::
This decorator is deprecated! Use :py:data:`tbot.ctx` instead:
.. code-block:: python
@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.
.. versionchanged:: 0.10.0
This decorator is now officially deprecated in favor of the
:ref:`context` mechanism.
"""
@functools.wraps(tc)
def wrapped(
arg: typing.Union[selectable.LabHost, linux.LinuxShell, None] = None,
*args: typing.Any,
**kwargs: typing.Any,
) -> typing.Any:
with contextlib.ExitStack() as cx:
lh: selectable.LabHost
lnx: linux.LinuxShell
# Acquire LabHost
if arg is None:
lh = cx.enter_context(selectable.acquire_lab())
elif isinstance(arg, linux.Lab):
lh = cx.enter_context(arg) # type: ignore
elif not isinstance(arg, linux.LinuxShell):
raise TypeError(
f"Argument to {tc!r} must either be a lab-host or a board linux (found {arg!r})"
)
# Acquire Linux
if arg is None or isinstance(arg, linux.Lab):
b = cx.enter_context(selectable.acquire_board(lh))
lnx = cx.enter_context(selectable.acquire_linux(b))
else:
lnx = cx.enter_context(arg)
return tc(lnx, *args, **kwargs)
# Adjust annotation
argname = tc.__code__.co_varnames[0]
wrapped.__annotations__[argname] = typing.Union[
selectable.LabHost, linux.LinuxShell, None
]
return typing.cast(F_linux, wrapped)