Getting Started¶
Warning
This is the old tbot introduction. The new version can be found at
First Steps¶
tbot works out of the box. You can run it’s selftests like this:
~$ tbot selftest
(If this does not work, please contact the developers. This should not be the case)
Now, let’s create some example testcase. Start by creating a file named tc.py
in
your current directory. Later you will see that you can also name it differently or
have multiple files, but for now, this is the easiest way. Add the following content:
1 2 3 4 5 6 7 | import tbot
@tbot.testcase
def my_awesome_testcase() -> None:
with tbot.acquire_lab() as lh:
name = lh.exec0("uname", "-n").strip()
tbot.log.message(f"Hello {name}!")
|
If you did everything correctly, running
~$ tbot my_awesome_testcase
should greet you host. Let’s disect the code from above:
1 2 3 | @tbot.testcase
def my_awesome_testcase() -> None:
...
|
This is just a normal python function for our testcase. The tbot.testcase()
decorator tells tbot that it should be treated as a testcase. In practice this
means that tbot will allow calling it from the commandline and will hook into
the call so it can gather log data about it.
with tbot.acquire_lab() as lh:
...
To understand this line, we first need to get to know one of the core concepts of tbot:
Machines (Machine
). Every host tbot interacts with is called a machine.
That includes the labhost, which we use here, a buildhost where your code might be compiled,
the board you are testing, or any other host you want tbot to connect to. There are different kinds
of machines. Our labhost is special, because it is the base from where connections to other host
are made.
Machines should always be used inside a with
statement to ensure proper cleanup in any
case. This is especially important with boardmachines, because if this is not done, the board
might not be turned off after the tests.
The line you see here requests a new labhost object from tbot so we can interact with it. As you will see later, this is not quite the way you would do this normally, but for this simple example it is good enough.
name = lh.exec0("uname", "-n").strip()
Now that we have the ability to interact with the labhost, let’s do so: We call
uname -n
to greet the users machine. Note, that each argument is passed separately to
exec0()
. The reason for this is that it ensures everything
will be properly escaped and there are no accidental mistakes. For special characters there is
a different notation as you will see later.
The strip()
is needed, because the command output contains the trailing newline, which we don’t
want in this case.
tbot.log.message(f"Hello {name}!")
tbot.log.message()
is basically tbot’s equivalent of print()
. The most important difference is, that it does not
only print it to the terminal, but also store it in the logfile.
Note
tbot has different Verbosity levels:
QUIET
: Only show testcases that are calledINFO
: Show info messages, such as those created bytbot.log.message()
COMMAND
: Show all commands that are called on the various machineSTDOUT
: Also show commands outputsCHANNEL
: Show everything received on all channels, useful for debugging
The default is INFO
. You can increase the Verbosity using -v
and decrease it using -q
.
Writing Testcases¶
As mentioned above, testcases calling tbot.acquire_lab()
is not the best way to do it. Why? Well, imagine,
each testcase that is called would create a new ssh connection to your labhost. This would be really
inefficient. The easiest solution is to require the lab as a parameter like this:
1 2 3 4 5 6 | import tbot
from tbot.machine import linux
@tbot.testcase
def my_testcase(lab: linux.LabHost) -> None:
...
|
This has the big disadvantage that a testcase like this can’t be called from tbot’s commandline, because where would it get that parameter from?
The solution is a hybrid and looks like the following:
1 2 3 4 5 6 7 8 9 10 11 | import typing
import tbot
from tbot.machine import linux
@tbot.testcase
def my_testcase(
lab: typing.Optional[linux.LabHost] = None,
) -> None:
with lab or tbot.acquire_lab() as lh:
name = lh.exec0("uname", "-n").strip()
tbot.log.message(f"Hello {name}!")
|
This is one of my ‘recipes’. These are code snippets that you will reuse all the time while using tbot. There are a lot more, for different tasks. Take a look at the Recipes page.
Note
In this documentation and in the tbot sources, type annotations are used everywhere. This allows
the use of a static type-checker such as mypy
, which makes finding bugs before you even run
the code a lot easier. Of course, this is optional, the following code would work just as well:
1 2 3 4 5 6 7 | import tbot
@tbot.testcase
def my_testcase(lab = None):
with lab or tbot.acquire_lab() as lh:
name = lh.exec0("uname", "-n")
tbot.log.message(f"Hello {name}!")
|
Calling other testcases is just as easy as calling a python function. From your perspective, a testcase is just a python function. If you want to call testcases from other files, import them like you would with a python module.
tbot contains a library of testcases for common tasks that you can make use of. Take a look at tbot.tc
.
Machine Interaction¶
Linux¶
All LinuxMachine
implement three methods for executing commands:
exec()
, exec0()
,
and test()
.
exec0()
is just a wrapper around
exec()
that ensures the return code of the command is 0
.
test()
returns True
if the command finished with return
code 0
and False
otherwise.
Both take the command as one argument per commandline parameter. For example:
1 2 3 | output = m.exec0("uname", "-n")
output = m.exec0("dmesg")
output = m.exec0("echo", "$!#?")
|
tbot will ensure that arguments are properly escaped, so you can pass in anything without worrying. This poses a problem, when you need special syntaxes. For example when you try to pipe the output of one command into another command. To do this in tbot, use code like the following:
1 2 3 | from tbot.machine import linux
usb_msg = m.exec0("dmesg", linux.Pipe, "grep", "usb")
|
This is not the only special parameter you can use:
Pipe
: A|
for piping command output to another commandThen
: A;
for running multiple commandsBackground
: A&
for running a command in the backgroundAndThen
: A&&
for chaining commandsOrElse
: A||
for error handling
There are even more, for more complex use cases:
F()
: Format string, for complex argument construction. Generally, you won’t need this, because you can just pass each parameter separately. An example, whereF()
is needed is a parameter that contains a path. Eg:# Add a path to $PATH m.exec0("export", linux.F("PATH={}:{}", mypath, m.env("PATH")))
What happens here? First of all,
m.env("PATH")
retrieves the current path. Then, we uselinux.F
and a format string to create the parameter. You can’t use an f-String in this case, because you can’t trivially turn a tbot path into a string.Env()
: Environment variable expansion. Sometimes you want to give an environment variable as a parameter. You can uselinux.Env
for exactly that. Example:m.exec0("echo", "Compiler:", linux.Env("CC"))
This isn’t the best way to do it, though. I highly reccomend using the following code instead:
m.exec0("/bin/ls", "-1", m.env("HOME"))
env()
will retrieve the value of the environment variable and return it as a string. The benefit of doing it this way is, that the value will be visible in the logfile and can be read when debugging a failure later on. If you uselinux.Env
, the log (and tbot) will never actually see the value of the environment variable and you can only guess what it was.Raw()
: Raw string if tbot isn’t expressive enough for your usecase. Use this only when no other option works.
Another thing tbot handles specially is paths. A Path
can be
created like this:
1 2 3 | from tbot.machine import linux
p = linux.Path(machine, "/foo/bar")
|
p
is now a Path
. tbot’s paths are based on
python’s pathlib
so you can use all the usual methods / operators:
1 2 3 4 5 | file_in_p = p / "dirname" / "file.txt"
if not p.exists():
...
if not p.is_dir():
raise RuntimeError(f"{p} must be a directory!")
|
tbot’s paths have a very nice property: They are bound to the host they were created with. This means that you cannot accidentally use a path on a wrong machine:
1 2 3 4 5 6 7 | m = tbot.acquire_lab()
lnx = tbot.acquire_linux(...)
p = linux.Path(m, "/path/to/somewhere/file.txt")
# This will raise an Exception and will be catched by a static typechecker like mypy:
content = lnx.exec0("cat", p)
|
Board¶
Interacting with the board is similar to interacting with a host like the labhost. The only difference is that this time, we need to first initialize the board:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | with tbot.acquire_board(lh) as b:
with tbot.acquire_uboot(b) as ub:
ub.exec0("version")
# Now boot into Linux
with tbot.acquire_linux(ub) as lnx:
lnx.exec0("uname", "-a")
# You can also boot directly into Linux:
# (Some boards might not even support intercepting
# U-Boot first)
with tbot.acquire_board(lh) as b:
with tbot.acquire_linux(b) as lnx:
lnx.exec0("uname", "-a")
|
Note
A pattern similar to the one above can be used to write testcases that can either be used from the commandline or supplied with a board-machine:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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.UBootMachine] = 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))
...
|
Again, take a look at the Testcase with U-Boot section on the Recipes page.
Interactive¶
One convenience function of tbot is allowing the user to directly access most machines’ shells. There are two ways to do so.
Calling one of the
interactive_lab
,interactive_build
,interactive_board
,interactive_uboot
interactive_linux
testcases. This is the most straight forward. It might look like this:~$ tbot -l labs/mylab.py -b boards/myboard.py interactive_uboot
Calling
machine.interactive()
in your testcase. For example:1 2 3 4 5 6 7 8 9
with tbot.acquire_board(lh) as b: with tbot.acquire_linux(b) as lnx: lnx.exec0("echo", "Doing some setup work") # Might raise an Exception if tbot was not able to reaquire the shell after # the interactive session lnx.interactive() lnx.exec0("echo", "Continuing testcase after the user made some adjustments")