TestParameters.rst 19.9 KB
Newer Older
1 2
.. _test-parameters:

3 4 5 6 7 8 9
===============
Test parameters
===============

.. note:: This section describes in detail what test parameters are and how
   the whole variants mechanism works in Avocado. If you're interested in the
   basics, see :ref:`accessing-test-parameters` or practical view by examples
10
   in :ref:`yaml-to-mux-plugin`.
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

Avocado allows passing parameters to tests, which effectively results in
several different variants of each test. These parameters are available in
(test's) ``self.params`` and are of
:class:`avocado.core.varianter.AvocadoParams` type.

The data for ``self.params`` are supplied by
:class:`avocado.core.varianter.Varianter` which asks all registered plugins
for variants or uses default when no variants are defined.

Overall picture of how the params handling works is:

.. following figure is not really a C code, but it renders well and it
   increases the visibility.

.. code-block:: c

       +-----------+
29 30 31
       |           |  // Test uses AvocadoParams, with content either from
       |   Test    |  // a variant or from the test parameters given by
       |           |  // "--test-parameters"
32
       +-----^-----+
33
             |
34 35
             |
       +-----------+
36
       |  Runner   |  // iterates through tests and variants to run all
37 38 39
       +-----^-----+  // desired combinations specified by "--execution-order".
             |        // if no variants are produced by varianter plugins,
             |        // use the test parameters given by "--test-parameters"
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
             |
   +-------------------+ provide variants +-----------------------+
   |                   |<-----------------|                       |
   | Varianter API     |                  | Varianter plugins API |
   |                   |----------------->|                       |
   +-------------------+  update defaults +-----------------------+
             ^                                ^
             |                                |
             |  // default params injected    |  // All plugins are invoked
   +--------------------------------------+   |  // in turns
   | +--------------+ +-----------------+ |   |
   | | avocado-virt | | other providers | |   |
   | +--------------+ +-----------------+ |   |
   +--------------------------------------+   |
                                              |
                 +----------------------------+-----+
                 |                                  |
                 |                                  |
                 v                                  v
       +--------------------+           +-------------------------+
       | yaml_to_mux plugin |           | Other variant plugin(s) |
       +-----^--------------+           +-------------------------+
             |
             |  // yaml is parsed to MuxTree,
             |  // multiplexed and yields variants
       +---------------------------------+
       | +------------+ +--------------+ |
       | | --mux-yaml | | --mux-inject | |
       | +------------+ +--------------+ |
       +---------------------------------+


Let's introduce the basic keywords.

TreeNode
~~~~~~~~

:class:`avocado.core.tree.TreeNode`

Is a node object allowing to create tree-like structures with
parent->multiple_children relations and storing params. It can
also report it's environment, which is set of params gathered
from root to this node. This is used in tests where instead of
passing the full tree only the leaf nodes are passed and their
environment represents all the values of the tree.

AvocadoParams
~~~~~~~~~~~~~

:class:`avocado.core.varianter.AvocadoParams`

91 92 93 94
Is a "database" of params present in every (instrumented) avocado
test.  It's produced during :class:`avocado.core.test.Test`'s
``__init__`` from a `variant`_. It accepts a list of `TreeNode`_
objects; test name :class:`avocado.core.test.TestID` (for logging
95
purposes) and a list of default paths (`Parameter Paths`_).
96 97 98 99 100 101 102 103 104 105 106 107 108 109

In test it allows querying for data by using::

   self.params.get($name, $path=None, $default=None)

Where:

* name - name of the parameter (key)
* path - where to look for this parameter (when not specified uses mux-path)
* default - what to return when param not found

Each `variant`_ defines a hierarchy, which is preserved so `AvocadoParams`_
follows it to return the most appropriate value or raise Exception on error.

110 111
Parameter Paths
~~~~~~~~~~~~~~~
112 113 114 115 116 117 118 119 120 121 122 123 124

As test params are organized in trees, it's possible to have the same
variant in several locations. When they are produced from the same
`TreeNode`_, it's not a problem, but when they are a different values
there is no way to distinguish which should be reported. One way is
to use specific paths, when asking for params, but sometimes, usually
when combining upstream and downstream variants, we want to get our
values first and fall-back to the upstream ones when they are not found.

For example let's say we have upstream values in ``/upstream/sleeptest``
and our values in ``/downstream/sleeptest``. If we asked for a value using
path ``"*"``, it'd raise an exception being unable to distinguish whether
we want the value from ``/downstream`` or ``/upstream``. We can set the
125
parameter paths to ``["/downstream/*", "/upstream/*"]`` to make all relative
126 127 128
calls (path starting with ``*``) to first look in nodes in ``/downstream``
and if not found look into ``/upstream``.

129 130
More practical overview of parameter paths is in :ref:`yaml-to-mux-plugin`
in :ref:`yaml-to-mux-resolution-order` section.
131 132 133 134

Variant
~~~~~~~

135 136 137
Variant is a set of params produced by `Varianter`_s and passed to the
test by the test runner as ``params`` argument. The simplest variant
is ``None``, which still produces an empty `AvocadoParams`_. Also, the
138
`Variant`_ can also be a ``tuple(list, paths)`` or just the
139
``list`` of :class:`avocado.core.tree.TreeNode` with the params.
140

A
Amador Pahim 已提交
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
Dumping/Loading Variants
~~~~~~~~~~~~~~~~~~~~~~~~

Depending on the number of parameters, generating the Variants can be very
compute intensive. As the Variants are generated as part of the Job execution,
that compute intensive task will be executed by the systems under test, causing
a possibly unwanted cpu load on those systems.

To avoid such situation, you can acquire the resulting JSON serialized variants
file, generated out of the variants computation, and load that file on the
system where the Job will be executed.

There are two ways to acquire the JSON serialized variants file:

- Using the ``--json-variants-dump`` option of the ``avocado variants``
  command::

    $ avocado variants --mux-yaml examples/yaml_to_mux/hw/hw.yaml --json-variants-dump variants.json
    ...

    $ file variants.json
    variants.json: ASCII text, with very long lines, with no line terminators

- Getting the auto-generated JSON serialized variants file after a Avocado Job
  execution::

    $ avocado run passtest.py --mux-yaml examples/yaml_to_mux/hw/hw.yaml
    ...

    $ file $HOME/avocado/job-results/latest/jobdata/variants.json
    $HOME/avocado/job-results/latest/jobdata/variants.json: ASCII text, with very long lines, with no line terminators

Once you have the ``variants.json`` file, you can load it on the system where
the Job will take place::

   $ avocado run passtest.py --json-variants-load variants.json
   JOB ID     : f2022736b5b89d7f4cf62353d3fb4d7e3a06f075
   JOB LOG    : $HOME/avocado/job-results/job-2018-02-09T14.39-f202273/job.log
    (1/6) passtest.py:PassTest.test;intel-scsi-56d0: PASS (0.04 s)
    (2/6) passtest.py:PassTest.test;intel-virtio-3d4e: PASS (0.02 s)
    (3/6) passtest.py:PassTest.test;amd-scsi-fa43: PASS (0.02 s)
    (4/6) passtest.py:PassTest.test;amd-virtio-a59a: PASS (0.02 s)
    (5/6) passtest.py:PassTest.test;arm-scsi-1c14: PASS (0.03 s)
    (6/6) passtest.py:PassTest.test;arm-virtio-5ce1: PASS (0.04 s)
   RESULTS    : PASS 6 | ERROR 0 | FAIL 0 | SKIP 0 | WARN 0 | INTERRUPT 0 | CANCEL 0
   JOB TIME   : 0.51 s
   JOB HTML   : $HOME/avocado/job-results/job-2018-02-09T14.39-f202273/results.html

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
Varianter
~~~~~~~~~

:class:`avocado.core.varianter.Varianter`

Is an internal object which is used to interact with the variants mechanism
in Avocado. It's lifecycle is compound of two stages. First it allows
the core/plugins to inject default values, then it is parsed and
only allows querying for values, number of variants and such.

Example workflow of `avocado run passtest.py -m example.yaml` is::

   avocado run passtest.py -m example.yaml
     |
     + parser.finish -> Varianter.__init__  // dispatcher initializes all plugins
     |
     + $PLUGIN -> args.default_avocado_params.add_default_param  // could be used to insert default values
     |
     + job.run_tests -> Varianter.is_parsed
     |
     + job.run_tests -> Varianter.parse
     |                     // processes default params
     |                     // initializes the plugins
     |                     // updates the default values
     |
     + job._log_variants -> Varianter.to_str  // prints the human readable representation to log
     |
     + runner.run_suite -> Varianter.get_number_of_tests
     |
     + runner._iter_variants -> Varianter.itertests  // Yields variants

In order to allow force-updating the `Varianter`_ it supports
``ignore_new_data``, which can be used to ignore new data. This is used
by :doc:`Replay` to replace the current run `Varianter`_ with the one
loaded from the replayed job. The workflow with ``ignore_new_data`` could
look like this::

   avocado run --replay latest -m example.yaml
     |
     + $PLUGIN -> args.default_avocado_params.add_default_param  // could be used to insert default values
     |
     + replay.run -> Varianter.is_parsed
     |
     + replay.run  // Varianter object is replaced with the replay job's one
     |             // Varianter.ignore_new_data is set
     |
     + $PLUGIN -> args.default_avocado_params.add_default_param  // is ignored as new data are not accepted
     |
     + job.run_tests -> Varianter.is_parsed
     |
     + job._log_variants -> Varianter.to_str
     |
     + runner.run_suite -> Varianter.get_number_of_tests
     |
     + runner._iter_variants -> Varianter.itertests

The `Varianter`_ itself can only produce an empty variant with the
`Default params`_, but it invokes all `Varianter plugins`_ and if any
of them reports variants it yields them instead of the default variant.



Default params
~~~~~~~~~~~~~~

254 255 256 257 258 259 260 261
The `Default params`_ is a mechanism to specify default values in
`Varianter`_ or `Varianter plugins`_. Their purpose is usually to
define values dependent on the system which should not affect the
test's results. One example is a qemu binary location which might
differ from one host to another host, but in the end they should
result in qemu being executable in test. For this reason the `Default
params`_ do not affects the test's variant-id (at least not in the
official `Varianter plugins`_).
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276

These params can be set from plugin/core by getting ``default_avocado_params``
from ``args`` and using::

    default_avocado_params.add_default_parma(self, name, key, value, path=None)

Where:

* name - name of the plugin which injects data (not yet used for anything,
  but we plan to allow white/black listing)
* key - the parameter's name
* value - the parameter's value
* path - the location of this parameter. When the path does not exists yet,
  it's created out of `TreeNode`_.

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
Test parameters
~~~~~~~~~~~~~~~

This is an Avocado core feature, that is, it's not dependent on any
varianter plugin.  In fact, it's only active when no Varianter plugin
is used and produces a valid variant.

Avocado will use those simple parameters, and will pass them to all
tests in a job execution.  This is done on the command line via
``--test-parameters``, or simply, ``-p``.  It can be given multiple
times for multiple parameters.

Because Avocado parameters do not have a mechanism to define their
types, test code should always consider that a parameter value is a
string, and convert it to the appropriate type.

.. note:: Some varianter plugins would implicitly set parameters
   with different data types, but given that the same test can be
   used with different, or none, varianter plugins, it's safer if
   the test does an explicit check or type conversion.

Because the :class:`avocado.core.varianter.AvocadoParams` mandates the
concept of a parameter path (a legacy of the tree based Multiplexer)
and these test parameters are flat, those test parameters are placed
in the ``/`` path.  This is to ensure maximum compatibility with tests
that do not choose an specific parameter location.

304 305 306
Varianter plugins
~~~~~~~~~~~~~~~~~

307
:class:`avocado.core.plugin_interfaces.Varianter`
308 309 310

A plugin interface that can be used to build custom plugins which
are used by `Varianter`_ to get test variants. For inspiration see
311 312 313
:class:`avocado_varianter_yaml_to_mux.YamlToMux` which is an
optional varianter plugin. Details about this plugin can be
found here :ref:`yaml-to-mux-plugin`.
314 315 316 317 318 319 320 321 322 323 324

Multiplexer
~~~~~~~~~~~

:mod:`avocado.core.mux`

``Multiplexer`` or simply ``Mux`` is an abstract concept, which was
the basic idea behind the tree-like params structure with the support
to produce all possible variants. There is a core implementation of
basic building blocks that can be used when creating a custom plugin.
There is a demonstration version of plugin using this concept in
325 326
:mod:`avocado_varianter_yaml_to_mux`
which adds a parser and then
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
uses this multiplexer concept to define an avocado plugin to produce
variants from ``yaml`` (or ``json``) files.


Multiplexer concept
===================

As mentioned earlier, this is an in-core implementation of building
blocks intended for writing `Varianter plugins`_ based on a tree
with `Multiplex domains`_ defined. The available blocks are:

* `MuxTree`_ - Object which represents a part of the tree and handles
  the multiplexation, which means producing all possible variants
  from a tree-like object.
* `MuxPlugin`_ - Base class to build `Varianter plugins`_
* ``MuxTreeNode`` - Inherits from `TreeNode`_ and adds the support for
  control flags (``MuxTreeNode.ctrl``) and multiplex domains
  (``MuxTreeNode.multiplex``).

And some support classes and methods eg. for filtering and so on.

Multiplex domains
~~~~~~~~~~~~~~~~~

A default `AvocadoParams`_ tree with variables could look like this::

   Multiplex tree representation:
    ┣━━ paths
    ┃     → tmp: /var/tmp
    ┃     → qemu: /usr/libexec/qemu-kvm
    ┗━━ environ
        → debug: False

The multiplexer wants to produce similar structure, but also to be able
to define not just one variant, but to define all possible combinations
and then report the slices as variants. We use the term
`Multiplex domains`_ to define that children of this node are not just
different paths, but they are different values and we only want one at
a time. In the representation we use double-line to visibily distinguish
between normal relation and multiplexed relation. Let's modify our
example a bit::

   Multiplex tree representation:
    ┣━━ paths
    ┃     → tmp: /var/tmp
    ┃     → qemu: /usr/libexec/qemu-kvm
    ┗━━ environ
         ╠══ production
         ║     → debug: False
         ╚══ debug
               → debug: True

The difference is that ``environ`` is now a ``multiplex`` node and it's
children will be yielded one at a time producing two variants::

   Variant 1:
    ┣━━ paths
    ┃     → tmp: /var/tmp
    ┃     → qemu: /usr/libexec/qemu-kvm
    ┗━━ environ
         ┗━━ production
               → debug: False
   Variant 2:
    ┣━━ paths
    ┃     → tmp: /var/tmp
    ┃     → qemu: /usr/libexec/qemu-kvm
    ┗━━ environ
         ┗━━ debug
               → debug: False

Note that the ``multiplex`` is only about direct children, therefore
the number of leaves in variants might differ::

   Multiplex tree representation:
    ┣━━ paths
    ┃     → tmp: /var/tmp
    ┃     → qemu: /usr/libexec/qemu-kvm
    ┗━━ environ
         ╠══ production
         ║     → debug: False
         ╚══ debug
              ┣━━ system
              ┃     → debug: False
              ┗━━ program
                    → debug: True

Produces one variant with ``/paths`` and ``/environ/production`` and
other variant with ``/paths``, ``/environ/debug/system`` and
``/environ/debug/program``.

As mentioned earlier the power is not in producing one variant, but
in defining huge scenarios with all possible variants. By using
tree-structure with multiplex domains you can avoid most of the
ugly filters you might know from Jenkin's sparse matrix jobs.
For comparison let's have a look at the same example in avocado::

   Multiplex tree representation:
    ┗━━ os
         ┣━━ distro
         ┃    ┗━━ redhat
         ┃         ╠══ fedora
         ┃         ║    ┣━━ version
         ┃         ║    ┃    ╠══ 20
         ┃         ║    ┃    ╚══ 21
         ┃         ║    ┗━━ flavor
         ┃         ║         ╠══ workstation
         ┃         ║         ╚══ cloud
         ┃         ╚══ rhel
         ┃              ╠══ 5
         ┃              ╚══ 6
         ┗━━ arch
              ╠══ i386
              ╚══ x86_64

Which produces::

   Variant 1:    /os/distro/redhat/fedora/version/20, /os/distro/redhat/fedora/flavor/workstation, /os/arch/i386
   Variant 2:    /os/distro/redhat/fedora/version/20, /os/distro/redhat/fedora/flavor/workstation, /os/arch/x86_64
   Variant 3:    /os/distro/redhat/fedora/version/20, /os/distro/redhat/fedora/flavor/cloud, /os/arch/i386
   Variant 4:    /os/distro/redhat/fedora/version/20, /os/distro/redhat/fedora/flavor/cloud, /os/arch/x86_64
   Variant 5:    /os/distro/redhat/fedora/version/21, /os/distro/redhat/fedora/flavor/workstation, /os/arch/i386
   Variant 6:    /os/distro/redhat/fedora/version/21, /os/distro/redhat/fedora/flavor/workstation, /os/arch/x86_64
   Variant 7:    /os/distro/redhat/fedora/version/21, /os/distro/redhat/fedora/flavor/cloud, /os/arch/i386
   Variant 8:    /os/distro/redhat/fedora/version/21, /os/distro/redhat/fedora/flavor/cloud, /os/arch/x86_64
   Variant 9:    /os/distro/redhat/rhel/5, /os/arch/i386
   Variant 10:    /os/distro/redhat/rhel/5, /os/arch/x86_64
   Variant 11:    /os/distro/redhat/rhel/6, /os/arch/i386
   Variant 12:    /os/distro/redhat/rhel/6, /os/arch/x86_64

Versus Jenkin's sparse matrix::

   os_version = fedora20 fedora21 rhel5 rhel6
   os_flavor = none workstation cloud
   arch = i386 x86_64

   filter = ((os_version == "rhel5").implies(os_flavor == "none") &&
             (os_version == "rhel6").implies(os_flavor == "none")) &&
            !(os_version == "fedora20" && os_flavor == "none") &&
            !(os_version == "fedora21" && os_flavor == "none")

Which is still relatively simple example, but it grows dramatically with
inner-dependencies.

MuxPlugin
~~~~~~~~~

:class:`avocado.core.mux.MuxPlugin`

Defines the full interface required by
476 477
:class:`avocado.core.plugin_interfaces.Varianter`. The plugin writer
should inherit from this ``MuxPlugin``, then from the ``Varianter``
478 479
and call the::

480
   self.initialize_mux(root, paths, debug)
481 482 483 484 485

Where:

* root - is the root of your params tree (compound of `TreeNode`_ -like
  nodes)
486
* paths - is the `Parameter paths`_ to be used in test with all variants
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
* debug - whether to use debug mode (requires the passed tree to be
  compound of ``TreeNodeDebug``-like nodes which stores the origin
  of the variant/value/environment as the value for listing purposes
  and is __NOT__ intended for test execution.

This method must be called before the `Varianter`_'s second stage
(the latest opportunity is during ``self.update_defaults``). The
`MuxPlugin`_'s code will take care of the rest.

MuxTree
~~~~~~~

This is the core feature where the hard work happens. It walks the tree
and remembers all leaf nodes or uses list of `MuxTrees` when another
multiplex domain is reached while searching for a leaf.

When it's asked to report variants, it combines one variant of each
remembered item (leaf node always stays the same, but `MuxTree` circles
through it's values) which recursively produces all possible variants
of different `multiplex domains`_.