1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874
|
Usage
=====
.. _remote-usage:
Remote Access
-------------
As described in :ref:`remote-resources-and-places`, one of labgrid's main
features is granting access to boards connected to other hosts transparent for
the client.
To get started with remote access, take a look at
:ref:`remote-getting-started`.
Place Scheduling
~~~~~~~~~~~~~~~~
When sharing places between developers or with CI jobs, it soon becomes
necessary to manage who can access which places.
Developers often just need any place which has one of a group of identical
devices, while CI jobs should wait until the necessary place is free instead of
failing.
To support these use-cases, the coordinator has support for reserving places by
using a tag filter and an optional priority.
First, the places have to be tagged with the relevant key-value pairs:
.. code-block:: bash
$ labgrid-client -p board-1 set-tags board=imx6-foo
$ labgrid-client -p board-2 set-tags board=imx6-foo
$ labgrid-client -p board-3 set-tags board=imx8m-bar
$ labgrid-client -v places
Place 'board-1':
tags: bar=baz, board=imx6-foo, jlu=2, rcz=1
matches:
rl-test/Testport1/NetworkSerialPort
…
Place 'board-2':
tags: board=imx6-foo
matches:
rl-test/Testport2/NetworkSerialPort
…
Place 'board-3':
tags: board=imx8m-bar
matches:
rl-test/Testport3/NetworkSerialPort
…
Now, if you want to access any ``imx6-foo`` board, you could find that all are
already in use by someone else:
.. code-block:: bash
$ labgrid-client who
User Host Place Changed
rcz dude board-1 2019-08-06 12:14:38.446201
jenkins worker1 board-2 2019-08-06 12:52:44.762131
In this case, you can create a reservation.
You can specify any custom tags as part of the filter, as well as
``name=<place-name>`` to select only a specific place (even if it has no custom
tags).
.. code-block:: bash
$ labgrid-client reserve board=imx6-foo
Reservation 'SP37P5OQRU':
owner: rettich/jlu
token: SP37P5OQRU
state: waiting
filters:
main: board=imx6-foo
created: 2019-08-06 12:56:49.779982
timeout: 2019-08-06 12:57:49.779983
As soon as any matching place becomes free, the reservation state will change
from ``waiting`` to ``allocated``.
Then, you can use the reservation token prefixed by ``+`` to refer to the
allocated place for locking and usage.
While a place is allocated for a reservation, only the owner of the reservation
can lock that place.
.. code-block:: bash
$ labgrid-client wait SP37P5OQRU
owner: rettich/jlu
token: SP37P5OQRU
state: waiting
filters:
main: board=imx6-foo
created: 2019-08-06 12:56:49.779982
timeout: 2019-08-06 12:58:14.900621
…
owner: rettich/jlu
token: SP37P5OQRU
state: allocated
filters:
main: board=imx6-foo
allocations:
main: board-2
created: 2019-08-06 12:56:49.779982
timeout: 2019-08-06 12:58:46.145851
$ labgrid-client -p +SP37P5OQRU lock
acquired place board-2
$ labgrid-client reservations
Reservation 'SP37P5OQRU':
owner: rettich/jlu
token: SP37P5OQRU
state: acquired
filters:
main: board=imx6-foo
allocations:
main: board-2
created: 2019-08-06 12:56:49.779982
timeout: 2019-08-06 12:59:11.840780
$ labgrid-client -p +SP37P5OQRU console
When using reservation in a CI job or to save some typing, the ``labgrid-client
reserve`` command supports a ``--shell`` command to print code for evaluating
in the shell.
This sets the ``LG_TOKEN`` environment variable, which is then automatically
used by ``wait`` and expanded via ``-p +``.
.. code-block:: bash
$ eval `labgrid-client reserve --shell board=imx6-foo`
$ echo $LG_TOKEN
ZDMZJZNLBF
$ labgrid-client wait
owner: rettich/jlu
token: ZDMZJZNLBF
state: waiting
filters:
main: board=imx6-foo
created: 2019-08-06 13:05:30.987072
timeout: 2019-08-06 13:06:44.629736
…
owner: rettich/jlu
token: ZDMZJZNLBF
state: allocated
filters:
main: board=imx6-foo
allocations:
main: board-1
created: 2019-08-06 13:05:30.987072
timeout: 2019-08-06 13:06:56.196684
$ labgrid-client -p + lock
acquired place board-1
$ labgrid-client -p + show
Place 'board-1':
tags: bar=baz, board=imx6-foo, jlu=2, rcz=1
matches:
rettich/Testport1/NetworkSerialPort
acquired: rettich/jlu
acquired resources:
created: 2019-07-29 16:11:52.006269
changed: 2019-08-06 13:06:09.667682
reservation: ZDMZJZNLBF
Finally, to avoid calling the ``wait`` command explicitly, you can add
``--wait`` to the ``reserve`` command, so it waits until the reservation is
allocated before returning.
A reservation will time out after a short time, if it is neither refreshed nor
used by locked places.
Library
-------
labgrid can be used directly as a Python library, without the infrastructure
provided by the pytest plugin.
The labgrid library provides two ways to configure targets with resources and
drivers: either create the :any:`Target` directly or use :any:`Environment` to
load a configuration file.
.. note::
On exit of your script/application, labgrid will call ``cleanup()`` on the
targets using the python atexit module.
Targets
~~~~~~~
.. note::
In most cases it is easier to :ref:`use a complete environment from a YAML
file <usage_environments>` instead of manually creating and activating objects.
Nevertheless, we explain this in the following to clarify the underlying concepts,
and how to work with targets on a lower level, e.g. in strategies.
At the lower level, a :any:`Target` can be created directly:
.. doctest::
>>> from labgrid import Target
>>> t = Target('example')
Next, any required :any:`Resource` objects can be created, which each represent
a piece of hardware to be used with labgrid:
.. doctest::
>>> from labgrid.resource import RawSerialPort
>>> rsp = RawSerialPort(t, name=None, port='/dev/ttyUSB0')
.. note::
Since we support multiple drivers of the same type, resources and drivers
have a required ``name`` attribute. If you don't use multiple drivers of the
same type, you can set the name to ``None``.
Further on, a :any:`Driver` encapsulates logic how to work with resources.
Drivers need to be created on the :any:`Target`:
.. doctest::
>>> from labgrid.driver import SerialDriver
>>> sd = SerialDriver(t, name=None)
As the :any:`SerialDriver` declares a binding to a :any:`SerialPort`, the target binds it
to the resource object created above:
.. doctest::
>>> sd.port
RawSerialPort(target=Target(name='example', env=None), name=None, state=<BindingState.bound: 1>, avail=True, port='/dev/ttyUSB0', speed=115200)
>>> sd.port is rsp
True
Driver Activation
^^^^^^^^^^^^^^^^^
Before a bound driver can be used, it needs to be activated.
During activation, the driver makes sure that all hardware represented by the
resources it is bound to can be used, and, if necessary, it acquires the
underlying hardware on the OS level.
For example, activating a :any:`SerialDriver` makes sure that the hardware
represented by its bound :any:`RawSerialPort` object (e.g. something like
``/dev/ttyUSB0``) is available, and that it can only be used labgrid and not by
other applications while the :any:`SerialDriver` is activated.
If we use a car analogy here, binding is the process of screwing the car parts
together, and activation is igniting the engine.
After activation, we can use the driver to do our work:
.. testsetup:: driver-activation
from labgrid.resource import RawSerialPort
from labgrid.driver import SerialDriver
from labgrid import Target
t = Target('example')
rsp = RawSerialPort(t, name=None, port='/dev/ttyUSB0')
sd = SerialDriver(t, name=None)
sd.serial.open = Mock()
sd.serial.write = Mock(return_value=4)
.. doctest:: driver-activation
>>> t.activate(sd)
>>> sd.write(b'test')
4
If an underlying hardware resource is not available (or not available after a
certain timeout, depending on the driver), the activation step will raise an
exception, e.g.::
>>> t.activate(sd)
Traceback (most recent call last):
File "/usr/lib/python3.8/site-packages/serial/serialposix.py", line 288, in open
self.fd = os.open(self.portstr, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
FileNotFoundError: [Errno 2] No such file or directory: '/dev/ttyUSB0'
Active drivers can be accessed by class (any :any:`Driver <labgrid.driver>` or
:any:`Protocol <labgrid.protocol>`) using some syntactic sugar:
.. doctest::
>>> from labgrid import Target
>>> from labgrid.driver.fake import FakeConsoleDriver
>>> target = Target('main')
>>> console = FakeConsoleDriver(target, 'console')
>>> target.activate(console)
>>> target[FakeConsoleDriver]
FakeConsoleDriver(target=Target(name='main', env=None), name='console', state=<BindingState.active: 2>, txdelay=0.0)
>>> target[FakeConsoleDriver, 'console']
FakeConsoleDriver(target=Target(name='main', env=None), name='console', state=<BindingState.active: 2>, txdelay=0.0)
Driver Deactivation
^^^^^^^^^^^^^^^^^^^
Driver deactivation works in a similar manner:
.. testsetup:: driver-deactivation
from labgrid import Target
from labgrid.driver.fake import FakeConsoleDriver
target = Target('main')
console = FakeConsoleDriver(target, 'console')
target.activate(console)
.. doctest:: driver-deactivation
>>> target.deactivate(console)
[FakeConsoleDriver(target=Target(name='main', env=None), name='console', state=<BindingState.bound: 1>, txdelay=0.0)]
Drivers need to be deactivated in the following cases:
* Some drivers have internal logic depending on the state of the target.
For example, the :any:`ShellDriver` remembers whether it has already logged
in to the shell.
If the target reboots, e.g. through a hardware watchdog timeout,
a power cycle, or by issuing a ``reboot`` command on the shell,
the ShellDriver's internal state becomes outdated,
and the ShellDriver needs to be deactivated and re-activated.
* One of the driver's bound resources is required by another driver which is to
be activated.
For example, the :any:`ShellDriver` and the :any:`BareboxDriver` both
require access to a :any:`SerialPort` resource.
If both drivers are bound to the same resource object, labgrid will
automatically deactivate the BareboxDriver when activating the ShellDriver.
Target Cleanup
^^^^^^^^^^^^^^
After you are done with the target, optionally call the cleanup method on your
target. While labgrid registers an ``atexit`` handler to cleanup targets, this has
the advantage that exceptions can be handled by your application:
.. testsetup:: target-cleanup
from labgrid import Target
target = Target('main')
.. doctest:: target-cleanup
>>> try:
... target.cleanup()
... except Exception as e:
... pass # your code here
.. _usage_environments:
Environments
~~~~~~~~~~~~
In practice, it is often useful to separate the `Target` configuration from the
code which needs to control the board (such as a test case or installation
script).
For this use-case, labgrid can construct targets from a configuration file in
YAML format:
.. code-block:: yaml
:name: example-env.yaml
targets:
example:
resources:
RawSerialPort:
port: '/dev/ttyUSB0'
drivers:
SerialDriver: {}
To parse this configuration file, use the :any:`Environment` class:
.. doctest::
>>> from labgrid import Environment
>>> env = Environment('example-env.yaml')
Using :any:`Environment.get_target`, the configured `Targets` can be retrieved
by name.
Without an argument, `get_target` would default to 'main':
.. doctest::
>>> t = env.get_target('example')
To access the target's console, the correct driver object can be found by using
:any:`Target.get_driver`:
.. testsetup:: get-driver
from labgrid import Environment
env = Environment('example-env.yaml')
t = env.get_target('example')
s = t.get_driver('SerialDriver', activate=False)
s.serial.open = Mock()
s.serial.write = Mock(return_value=4)
.. doctest:: get-driver
>>> cp = t.get_driver('ConsoleProtocol')
>>> cp
SerialDriver(target=Target(name='example', env=Environment(config_file='example-env.yaml')), name=None, state=<BindingState.active: 2>, txdelay=0.0, timeout=3.0)
>>> cp.write(b'test')
4
When using the ``get_driver`` method, the driver is automatically activated.
The driver activation will also wait for unavailable resources when needed.
For more information on the environment configuration files and the usage of
multiple drivers, see :ref:`configuration:Environment Configuration`.
pytest Plugin
-------------
labgrid includes a `pytest <http://pytest.org>`_ plugin to simplify writing tests which
involve embedded boards.
The plugin is configured by providing an environment config file
(via the --lg-env pytest option, or the LG_ENV environment variable)
and automatically creates the targets described in the environment.
These `pytest fixtures <http://docs.pytest.org/en/latest/fixture.html>`_ are provided:
env (session scope)
Used to access the :any:`Environment` object created from the configuration
file.
This is mostly used for defining custom fixtures at the test suite level.
target (session scope)
Used to access the 'main' :any:`Target` defined in the configuration file.
strategy (session scope)
Used to access the :any:`Strategy` configured in the 'main' :any:`Target`.
Command-Line Options
~~~~~~~~~~~~~~~~~~~~
The pytest plugin also supports the verbosity argument of pytest:
- ``-vv``: activates the step reporting feature, showing function parameters and/or results
- ``-vvv``: activates debug logging
This allows debugging during the writing of tests and inspection during test runs.
Other labgrid-related pytest plugin options are:
``--lg-env=LG_ENV`` (was ``--env-config=ENV_CONFIG``)
Specify a labgrid environment config file.
This is equivalent to labgrid-client's ``-c``/``--config``.
``--lg-coordinator=COORDINATOR_ADDRESS``
Specify labgrid coordinator gRPC address as ``HOST[:PORT]``.
Defaults to ``127.0.0.1:20408``.
This is equivalent to labgrid-client's ``-x``/``--coordinator``.
``--lg-log=[path to logfiles]``
Path to store console log file.
If option is specified without path the current working directory is used.
``--lg-colored-steps``
Previously enabled the ColoredStepReporter, which has been removed with the
StepLogger introduction.
Kept for compatibility reasons without effect.
``--lg-initial-state=STATE_NAME``
Sets the Strategy's initial state.
This is useful during development if the board is known to be in a defined
state already.
The Strategy used must implement the ``force()`` method.
See the shipped :any:`ShellStrategy` for an example.
``pytest --help`` shows these options in a separate *labgrid* section.
Environment Variables
~~~~~~~~~~~~~~~~~~~~~
LG_ENV
^^^^^^
Behaves like ``LG_ENV`` for :doc:`labgrid-client <man/client>`.
LG_PROXY
^^^^^^^^
Specifies a SSH proxy host to be used for port forwards to access the
coordinator. Network resources made available by the exporter will prefer their
own proxy, and only fallback to LG_PROXY.
See also :ref:`overview-proxy-mechanism`.
Simple Example
~~~~~~~~~~~~~~
As a minimal example, we have a target connected via a USB serial converter
('/dev/ttyUSB0') and booted to the Linux shell.
The following environment config file (``shell-example.yaml``) describes how to
access this board:
.. code-block:: yaml
:name: shell-example.yaml
targets:
main:
resources:
RawSerialPort:
port: '/dev/ttyUSB0'
drivers:
SerialDriver: {}
ShellDriver:
prompt: 'root@[\w-]+:[^ ]+ '
login_prompt: ' login: '
username: 'root'
We then add the following test in a file called ``test_example.py``:
.. code-block:: python
:name: test_shell.py
def test_echo(target):
command = target.get_driver('CommandProtocol')
result = command.run_check('echo OK')
assert 'OK' in result
To run this test, we simply execute pytest in the same directory with the
environment config:
.. testsetup:: pytest-example
from labgrid.driver import SerialDriver, ShellDriver
patch('serial.Serial').start()
patch.object(
SerialDriver,
'_read',
Mock(return_value=b'root@example:~ ')
).start()
patch.object(
ShellDriver,
'_run',
Mock(return_value=(['OK'], [], 0))
).start()
.. testcode:: pytest-example
:hide:
import pytest
plugins = ['labgrid.pytestplugin']
pytest.main(['--lg-env', 'shell-example.yaml', 'test_shell.py'], plugins)
.. testoutput:: pytest-example
:hide:
... 1 passed...
.. code-block:: bash
$ pytest --lg-env shell-example.yaml --verbose
============================= test session starts ==============================
platform linux -- Python 3.5.3, pytest-3.0.6, py-1.4.32, pluggy-0.4.0
…
collected 1 items
test_example.py::test_echo PASSED
=========================== 1 passed in 0.51 seconds ===========================
pytest has automatically found the test case and executed it on the target.
Custom Fixture Example
~~~~~~~~~~~~~~~~~~~~~~
When writing many test cases which use the same driver, we can get rid of some
common code by wrapping the `CommandProtocol` in a fixture.
As pytest always executes the ``conftest.py`` file in the test suite directory,
we can define additional fixtures there:
.. code-block:: python
:name: conftest_fixture.py
import pytest
@pytest.fixture(scope='session')
def command(target):
return target.get_driver('CommandProtocol')
With this fixture, we can simplify the ``test_example.py`` file to:
.. code-block:: python
:name: test_custom_fixture.py
def test_echo(command):
result = command.run_check('echo OK')
assert 'OK' in result
.. testcode:: pytest-example
:hide:
import pytest
plugins = ['labgrid.pytestplugin', 'conftest_fixture']
pytest.main(['--lg-env', 'shell-example.yaml', 'test_custom_fixture.py'], plugins)
.. testoutput:: pytest-example
:hide:
... 1 passed...
Strategy Fixture Example
~~~~~~~~~~~~~~~~~~~~~~~~
When using a :any:`Strategy` to transition the target between states, it is
useful to define a function scope fixture per state in ``conftest.py``:
.. code-block:: python
import pytest
@pytest.fixture(scope='function')
def switch_off(strategy, capsys):
with capsys.disabled():
strategy.transition('off')
@pytest.fixture(scope='function')
def bootloader_command(target, strategy, capsys):
with capsys.disabled():
strategy.transition('barebox')
return target.get_active_driver('CommandProtocol')
@pytest.fixture(scope='function')
def shell_command(target, strategy, capsys):
with capsys.disabled():
strategy.transition('shell')
return target.get_active_driver('CommandProtocol')
.. note::
The ``capsys.disabled()`` context manager is only needed when using the
:any:`ManualPowerDriver`, as it will not be able to access the console
otherwise.
See the corresponding `pytest documentation for details
<http://doc.pytest.org/en/latest/capture.html#accessing-captured-output-from-a-test-function>`_.
With the fixtures defined above, switching between bootloader and Linux shells
is easy:
.. code-block:: python
def test_barebox_initial(bootloader_command):
stdout = bootloader_command.run_check('version')
assert 'barebox' in '\n'.join(stdout)
def test_shell(shell_command):
stdout = shell_command.run_check('cat /proc/version')
assert 'Linux' in stdout[0]
def test_barebox_after_reboot(bootloader_command):
bootloader_command.run_check('true')
.. note::
The `bootloader_command` and `shell_command` fixtures use
:any:`Target.get_active_driver` to get the currently active `CommandProtocol`
driver (either :any:`BareboxDriver` or :any:`ShellDriver`).
Activation and deactivation of drivers is handled by the
:any:`BareboxStrategy` in this example.
The `Strategy` needs additional drivers to control the target.
Adapt the following environment config file (``strategy-example.yaml``) to your
setup:
.. code-block:: yaml
targets:
main:
resources:
RawSerialPort:
port: '/dev/ttyUSB0'
drivers:
ManualPowerDriver:
name: 'example-board'
SerialDriver: {}
BareboxDriver:
prompt: 'barebox@[^:]+:[^ ]+ '
ShellDriver:
prompt: 'root@[\w-]+:[^ ]+ '
login_prompt: ' login: '
username: 'root'
BareboxStrategy: {}
For this example, you should get a report similar to this:
.. code-block:: bash
$ pytest --lg-env strategy-example.yaml -v --capture=no
============================= test session starts ==============================
platform linux -- Python 3.5.3, pytest-3.0.6, py-1.4.32, pluggy-0.4.0
…
collected 3 items
test_strategy.py::test_barebox_initial
main: CYCLE the target example-board and press enter
PASSED
test_strategy.py::test_shell PASSED
test_strategy.py::test_barebox_after_reboot
main: CYCLE the target example-board and press enter
PASSED
========================== 3 passed in 29.77 seconds ===========================
Feature Flags
~~~~~~~~~~~~~
labgrid includes support for feature flags on a global and target scope.
Adding a ``@pytest.mark.lg_feature`` decorator to a test ensures it is only
executed if the desired feature is available:
.. code-block:: python
:name: test_feature_flags.py
import pytest
@pytest.mark.lg_feature("camera")
def test_camera(target):
pass
Here's an example environment configuration:
.. code-block:: yaml
:name: feature-flag-env.yaml
targets:
main:
features:
- camera
resources: {}
drivers: {}
.. testcode:: pytest-example
:hide:
import pytest
plugins = ['labgrid.pytestplugin']
pytest.main(['--lg-env', 'feature-flag-env.yaml', 'test_feature_flags.py'], plugins)
.. testoutput:: pytest-example
:hide:
... 1 passed...
This would run the above test, however the following configuration would skip the
test because of the missing feature:
.. code-block:: yaml
:name: feature-flag-skip-env.yaml
targets:
main:
features:
- console
resources: {}
drivers: {}
.. testcode:: pytest-example
:hide:
import pytest
plugins = ['labgrid.pytestplugin']
pytest.main(['--lg-env', 'feature-flag-skip-env.yaml', 'test_feature_flags.py'], plugins)
.. testoutput:: pytest-example
:hide:
... 1 skipped...
pytest will record the missing feature as the skip reason.
For tests with multiple required features, pass them as a list to pytest:
.. code-block:: python
:name: test_feature_flags_global.py
import pytest
@pytest.mark.lg_feature(["camera", "console"])
def test_camera(target):
pass
Features do not have to be set per target, they can also be set via the global
features key:
.. code-block:: yaml
:name: feature-flag-global-env.yaml
features:
- camera
targets:
main:
features:
- console
resources: {}
drivers: {}
.. testcode:: pytest-example
:hide:
import pytest
plugins = ['labgrid.pytestplugin']
pytest.main(['--lg-env', 'feature-flag-global-env.yaml', 'test_feature_flags_global.py'],
plugins)
.. testoutput:: pytest-example
:hide:
... 1 passed...
This YAML configuration would combine both the global and the target features.
Test Reports
~~~~~~~~~~~~
pytest-html
^^^^^^^^^^^
With the `pytest-html plugin <https://pypi.python.org/pypi/pytest-html>`_, the
test results can be converted directly to a single-page HTML report:
.. code-block:: bash
$ pip install pytest-html
$ pytest --lg-env shell-example.yaml --html=report.html
JUnit XML
^^^^^^^^^
JUnit XML reports can be generated directly by pytest and are especially useful for
use in CI systems such as `Jenkins <https://jenkins.io/>`_ with the `JUnit
Plugin <https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin>`_.
They can also be converted to other formats, such as HTML with `junit2html tool
<https://pypi.python.org/pypi/junit2html>`_:
.. code-block:: bash
$ pip install junit2html
$ pytest --lg-env shell-example.yaml --junit-xml=report.xml
$ junit2html report.xml
labgrid adds additional xml properties to a test run, these are:
- ENV_CONFIG: Name of the configuration file
- TARGETS: List of target names
- TARGET_{NAME}_REMOTE: optional, if the target uses a RemotePlace
resource, its name is recorded here
- PATH_{NAME}: optional, labgrid records the name and path
- PATH_{NAME}_GIT_COMMIT: optional, labgrid tries to record git sha1 values for every
path
- IMAGE_{NAME}: optional, labgrid records the name and path to the image
- IMAGE_{NAME}_GIT_COMMIT: optional, labgrid tries to record git sha1 values for every
image
Command-Line
------------
labgrid contains some command line tools which are used for remote access to
resources.
See :doc:`man/client`, :doc:`man/device-config` and :doc:`man/exporter` for
more information.
Advanced CLI features
~~~~~~~~~~~~~~~~~~~~~
This section of the manual describes advanced features that are supported by the
labgrid client CLI.
Sharing a place with a co-worker
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Labgrid client allows multiple people to access the same shared place,
even though locks can only be acquired by one person. To allow a coworker to use
a place use the ``allow`` command of labgrid client in conjunction with the
coworkers ``user`` and ``hostname``. As an example, with a places named
``example``, a user name ``john`` and a host named ``sirius``, the command looks
like this:
.. code-block:: bash
$ labgrid-client -p example allow sirius/john
To remove the allow it is currently necessary to unlock and lock the place.
|