On 08/08/2017 07:01 AM, Lukáš Doktor wrote:
> Hello guys,
> 
> I'm sorry for such a late response, I totally forgot about this email (thanks 
> to Ademar, your response reminded it to me).
> 
> Dne 7.8.2017 v 23:49 Ademar Reis napsal(a):
>> On Tue, Aug 01, 2017 at 03:37:34PM -0400, Cleber Rosa wrote:
>>> Even though Avocado has had a parameter passing system for
>>> instrumented tests almost from day one, it has been intertwined with
>>> the varianter (then multiplexer) and this is fundamentally wrong.  The
>>> most obvious example of this broken design is the `mux-inject` command
>>> line option::
>>>
>>>   --mux-inject [MUX_INJECT [MUX_INJECT ...]]
>>>                         Inject [path:]key:node values into the final
>>> multiplex
>>>                         tree.
>>>
>>> This is broken design not because such a varianter implementations can
>>> be tweaked over the command line, that's fine.  It's broken because it
>>> is the recommended way of passing parameters on the command line.
>>>
>>> The varianter (or any other subsystem) should be able to act as a
>>> parameter provider, but can not dictate that parameters must first be
>>> nodes/key/values of its own internal structure.
>>
>> Correct. It's broken because it violates several layers. There would
>> be nothing wrong with something like "--param [prefix:]<key:value>",
>> for example (more below).
>>
> Well I wouldn't call it broken. The implementation is fine we only lack other 
> providers which would allow to inject just params so people are abusing 
> `mux-inject` for that.
> 
>>>
>>> The proposed design
>>> ===================
>>>
>>> A diagram has been used on a few different occasions, to describe how
>>> the parameters and variants generation mechanism should be connected
>>> to a test and to the overall Avocado architecture.  Here it is, in its
>>> original form::
>>>
>>>    +------------------------------+
>>>    | Test                         |
>>>    +------------------------------+
>>>              |
>>>              |
>>>    +---------v---------+    +--------------------------------+
>>>    | Parameters System |--->| Variants Generation Plugin API |
>>>    +-------------------+    +--------------------------------+
>>>              ^                                ^
>>>              |                                |
>>>    +--------------------------------------+   |
>>>    | +--------------+ +-----------------+ |   |
>>>    | | avocado-virt | | other providers | |   |
>>>    | +--------------+ +-----------------+ |   |
>>>    +--------------------------------------+   |
>>>                                               |
>>>                  +----------------------------+-----+
>>>                  |                                  |
>>>                  |                                  |
>>>                  |                                  |
>>>        +--------------------+           +-------------------------+
>>>        | Multiplexer Plugin |           | Other variant plugin(s) |
>>>        +--------------------+           +-------------------------+
>>>              |
>>>              |
>>>        +-----v---------------------------+
>>>        | +------------+ +--------------+ |
>>>        | | --mux-yaml | | --mux-inject | |
>>>        | +------------+ +--------------+ |
>>>        +---------------------------------+
>>>
>>>
>>> Given that the "Parameter System" is the entry point into the parameters
>>> providers, it should provide two different interfaces:
>>>
>>>  1) An interface for its users, that is, developers writing
>>>     `avocado.Test` based tests
>>>
>>>  2) An interface for developers of additional providers, such as the
>>>     "avocado-virt" and "other providers" box on the diagram.
>>>
>>> The current state of the the first interface is the ``self.params``
>>> attribute.  Hopefully, it will be possible to keep its current interface,
>>> so that tests won't need any kind of compatibility adjustments.
>>
>> Right. The way I envision the parameters system includes a
>> resolution mechanism, the "path" currently used in params.get().
>> This adds extra specificity to the user who requests a parameter.
>>
>> But these parameters can be provided by any entity. In the diagram
>> above, they're part of the "Parameter System" box. Examples of
>> "other providers" could be support for a configuration file or a
>> "--param=[prefix:]<key:value>" command line option.
>>
>> The Test API to request parameters should be shared. To a test it
>> doesn't matter where the parameter is coming from. They're always
>> accessed through an API like:
>>
>>     params.get(<key>, [prefix], [fallback value])
>>
>> Where:
>>   * key (mandatory) is the configuration variable the user wants.
>>
>>   * prefix (optional) is used to find the right key and can be
>>     partially resolved, from right to left. As an example, a
>>     `params.get("bar", "name")` would be fulfilled by the parameter
>>     system if there are entries like "bar:name", "foobar:name" or
>>     "/foo/bar:name". We're using '/' in the multiplexer to
>>     separate branch levels, thus instead of "prefix", we're calling
>>     it "path", but the mechanism is the same and IMO should be
>>     generic to avoid confusion (a path at the parameter level is
>>     different than a path at the multiplexer level).
>>
>>     params.get() can be even more flexible, supporting regexps and
>>     blobs...  The objective of [prefix] is to serve as a filter and
>>     to guarantee the caller is getting the variable he wants. It's
>>     similar to a namespace.
>>
>>   * fallback (optional) is the value returned if the parameter
>>     system can't resolve prefix+key.
>>
>> Users who don't want any specificity and/or have a small test base
>> with a low chance of clashes could simply ignore the prefix both
>> when creating parameters and when making calls to params.get().
>>
>>>
>>> The second item will probably mean the definition of a new class to
>>> the ``avocado.core.plugin_interfaces`` module, together with a new
>>> dispatcher(-like) implementation in ``avocado.core.dispatcher``.
>>>
>>> Parameters availability: local .vs. external
>>> ============================================
>>>
>>> Right now, all parameters are given to the test at instantiation time.
>>> Let's say that in this scenario, all parameters are *local* to the
>>> test.  Local parameters have the benefit that the test is self
>>> contained and doesn't need any external communication.
>>>
>>> In theory, this is also a limitation, because all parameters must be
>>> available before the test is started.  Even if other parameter system
>>> implementations are possible, with a local approach, there would be a
>>> number of limitations.  For long running tests, that may depend on
>>> parameters generated during the test, this would be a blocker.  Also,
>>> if a huge number of parameters would be available (think of a huge
>>> cloud or database of parameters) they would all have to be copied to
>>> the test at instantiation time.  Finally, it also means that the
>>> source of parameters would need to be able to iterate over all the
>>> available parameters, so that they can be copied, which can be a
>>> further limitation for cascading implementations.
>>>
>>> An external approach to parameters, would be one that the test holds a
>>> handle to a broker of parameter providers.  The parameter resolution
>>> would be done at run time.  This avoids the copying of parameters, but
>>> requires runtime communication with the parameter providers.  This can
>>> make the test execution much more fragile and dependent on the external
>>> communication.  Even by minimizing the number of communication
>>> endpoints by communicating with the test runner only, it can still add
>>> significant overhead, latency and point of failures to the test
>>> execution.
>>>
>>> I believe that, at this time, the limitations imposed by local
>>> parameter availability do *not* outweigh the problems that an external
>>> approach can bring.  In the future, if advanced use cases require an
>>> external approach to parameters availability, this can be reevaluated.
>>
>> If I understand your point correctly, this is an implementation
>> detail. It depends on what the "contract" is between the test runner
>> (parameter provider) and the test being run (the user of
>> params.get()).
> The only difference for the scope of this RFC I see is that the "lazy" params 
> can't be merged with "static" params before test execution (otherwise it'd 
> effectively made them static as well). This is quite important difference to 
> consider further in the design...
> 

Exactly.  What we're leaning towards here is the simplest approach: all
parameters are "static" (as your call them) or "local" (as I called them).

>>
>> For example, should users assume parameters are dynamic and can
>> change during the lifetime of a test, an therefore two identical
>> calls to params.get() might return different values?  Should it be
>> possible to change params (something like params.put()) at runtime?
> Definitely no params changing.
> 

Agreed.

>>
>> (IMO the answer should be no to both questions).
>>
>> If you have something different in mind, then it would be
>> interesting to see some real use-cases.
>>
>>>
>>> Namespaces (AKA how/if should we merge parameters)
>>> ==================================================
>>>
>>> Currently, the parameter fetching interface already contains at its
>>> core the concept of paths[1].  In theory, this is sufficient to prevent
>>> clashes of keys with the same names, but intended to configure different
>>> aspects of a test.
>>>
>>> Now, with the proposed implementation of multiple providers to the
>>> parameter system, the question of how they will be combined comes up.
>>>
>>> One way is for each implementation to claim, based on some unique
>>> attribute such as its own name, a part of a tree path.  For instance,
>>> for two implementations:
>>>
>>>  1) variants
>>>  2) plain_ini
>>>
>>> Users could access parameters explicitly defined on those by referring
>>> to paths such as:
>>>
>>>    self.params.get('speed', path='/plain_ini', default=60)
>>>
>>> or
>>>
>>>    self.params.get('speed', path='/variants/path/inside/varianter',
>>> default=60)
>>>
>>> This clearly solves the clashes, but binds the implementation to the
>>> tests, which should be avoided at all costs.
>>
>> So you're providing this as an example of why it's a bad idea...
>> OK. :)
>>
> Yep, don't see this as a way to go. One should be able to execute the same 
> test with different providers (eg. json file with params generated by jenkins)
> 

Agreed.

>>>
>>> One other approach would be to merge everything into the tree root
>>> node.  By doing this, one parameter provider could effectively
>>> override the values in another parameter provider, given that both
>>> used the same paths for a number of parameters.
> This wouldn't be possible with "lazy" providers as it'd require iterating 
> through all params.
> 

Exactly.

>>>
>>> Yet another approach would be to *not* use paths, and resort to
>>> completely separate namespaces.  A parameter namespace would be an
>>> additional level of isolation, which can quickly become exceedingly
>>> complex.
> If I understood it correctly, this is what I'd go with. The way I envision 
> this is:
> 
> 1. varianter yields a variant, which also contains params in form of list of 
> namespaces associated with params `variant = [{"/run/variant": {"foo": 
> "bar"}},]`
> 2. runner attaches params from cmdline `cmdline = [{"/1": {"foo": "bar"}}, 
> {"/2": {"foo": "bar"}}]`
> 3. runner attaches params from avocado-virt plugin `virt = [{"/virt": 
> {"image_name": "foo"}]
> 4. runner executes test and passes params definition there
>     {"params": [("varianter", varianter), ("cmdline", cmdline), ("virt", 
> virt)],
>      "order": ["varianter", "__ALL_", "cmdline"]}
> 

Right, the general idea is sound.  Any reason why the runner itself
shouldn't prepare the *resulting* parameter data and pass it to the
test?  It's a matter of either passing data or code to the test.  I'd
rather pass data.

Think, for a second, about test replay.  If we pass parameter *data* to
the test, there's nothing else to solve here.  Serialization of that
data is trivial, and could even be a parameter provider itself (the only
or preferred one used at test replay time).

> Now the NewAvocadoParams object should create AvocadoParams per each provider 
> (params, cmdline, virt) being able to query params from any of them. The 
> process of getting params would get a bit more complicated for Avocado, but 
> for user nothing changes. Let's say user issues params.get("foo", "/*"):
> 
> 1. NewAvocadoParasms looks at the "order"
> 2. varianter is asked for a value, reports "bar", as this is a valid 
> response, no further params is queried and "bar" is returned
> 

I still find it much more deterministic to do that before the test kicks
in.  The test would have the resulting data for all the providers instead.

> Now for params.get("foo", "/1/*")
> 
> 1. NewAvocadoParasms looks at the "order"
> 2. varianter is asked and reports no match
> 3. __ALL__ => means ask providers without assigned priority, which applies to 
> "virt" in this example, which reports no match
> 4. cmdline is asked and returns "bar"
> 

Oh, special things are bad, and so is the "__ALL__" you're suggesting
here.  Also, just to be clear, do you mean special behavior to the
params generated by the varianter?

> And params.get("missing")
> 
> 1. NewAvocadoParasms looks at the "order"
> 2. varianter => no match
> 3. virt => no match
> 4. cmdline => no match
> 5. default is reported (None)
> 

Again, I fail to see why this shouldn't be done by the test runner instead.

> 
> The "order" is important, by default I'd suggest 
> varianter=>plugins=>params=>default_params (basically what Ademar suggested 
> and we all agreed many times), but it should be possible to set it if 
> necessary (eg. when user have multiple plugins and knows the order).
> 

This seems to indicate the varianter has a special role.  Can you
elaborate on that?

> Now in case of clash I believe it should report the clash even though further 
> providers might be able to report. Let's change the order to ["cmdline", 
> "__ALL__"] and query for params.get("foo", "/*")
> 
> 1. cmdline is asked, it raises ValueError("Leaves [/1, /2] contain key 
> "foo"\n  /1:foo:bar\n  /2:foo:bar") which is forwarded to the test.
> 
> This schema would work for "lazy" providers as it'd only be asked for 
> specific key and only when needed.
> 

The way I see this: clashes could be reported at merge time, as a
mega-uber-verbose option, but I'd refrain from it.  After that,
parameter data is clearly handed to a test.  The user running and
debugging the test, can clearly see the parameters available.  A verbose
mode can show something similar to the Avocado-VT cartesian config based
parameters.

- Cleber.

>>
>> I think using paths is confusing because it mixes concepts which are
>> exclusive to the multiplexer (a particular implementation of the
>> varianter) with an API that is shared by all other parameter
>> providers.
>>
>> For example, when you say "merge everything into the tree root
>> node", are you talking about namespace paths, or paths as used by
>> the multiplexer when the "!mux" keyword is present?
>> There is no difference between namespace path and path in multiplexer. The 
>> only difference is that multiplexer can provide a different slice, but the 
>> namespace is always the same, only not existing in some slices:
> 
>>>
>>> As can be seen from the section name, I'm not proposing one solution
>>> at this point, but hoping that a discussion on the topic would help
>>> achieve the best possible design.
>>
>> I think this should be abstract to the Test (in other words, not
>> exposed through any API). The order, priority and merge of
>> parameters is a problem to be solved at run-time by the test runner.
>>
>> All a test needs to "know" is that there's a parameter with the name
>> it wants.
>>
>> In the case of clashes, specifying a prioritization should be easy.
>> We could use a similar approach to how we prioritize Avocado's own
>> configuration.
>>
>> Example: from less important to top priorities, when resolving a
>> call to params.get():
>>
>>    * "default" value provided to params.get() inside the test code.
>>    * Params from /etc/avocado/global-variants.ini
>>    * Params from ~/avocado/global-variants.ini
>>    * Params from "--params=<file>"
>>    * Params from "--param=[prefix:]<key:value>"
>>    * Params from the variant generated from --mux-yaml=<file> (and
>>      using --mux-inject would have the same effect of changing
>>      <file> before using it)
>>
>> The key of this proposal is simplicity and scalability: it doesn't
>> matter if the user is running the test with the varianter, a simple
>> config file (--params=<file>) or passing some parameters by hand
>> (--param=key:value). The Test API and behavior are the same and the
>> users get a consistent experience.
> Yep. Basically there is a test writer and test user. Test writer should write 
> a test and ask for params. Test user should provide params which might also 
> include specific order, if needed (via cmdline or config).
> 
> Lukáš
> 
>>
>> Thanks.
>>    - Ademar
>>
>>> [1] -
>>> http://avocado-framework.readthedocs.io/en/52.0/api/core/avocado.core.html#avocado.core.varianter.AvocadoParams.get
>>
>>
>>
> 
> 

-- 
Cleber Rosa
[ Sr Software Engineer - Virtualization Team - Red Hat ]
[ Avocado Test Framework - avocado-framework.github.io ]
[  7ABB 96EB 8B46 B94D 5E0F  E9BB 657E 8D33 A5F2 09F3  ]

Attachment: signature.asc
Description: OpenPGP digital signature

Reply via email to