Dne 27.11.2014 v 14:06 Paolo Bonzini napsal(a):
On 26/11/2014 20:56, Lukáš Doktor wrote:
This looks amazing and should be doable in YAML. The `mux-default`
should not be needed. Let me explain the idea here. Do you remember the
ways to execute test from my email:
1) default variant
2) full test
3) all related
4) multiplex all
And the places where variants are stored:
A) yaml file added on cmdline (/hw, /os, ...)
B) each test's associated file (/tests/$name/duration,
/test/$name/function, ...)
Z) for default variables (new idea), let's say
`avocado.yaml` which would be 1 level yaml with all default variables
(image: "Fedora", passwd: "123456", ...)
When you execute test as 1), it'd take default values from Z) and than
get only first level values from B)
In case you execute 2), it'd again get default values from Z) and then
it'd parse the B) tree.
The 3) would need to parse Z), A) and B) and multiplex only mux-choices
specified in the B).
The 4) would parse Z), A) and B) and multiplex everything what is not
filtered out.
In my case you'd have:
1 and 2) command line switch --mux=none or --no-mux: take default values
from B and Z, apply mux-expands found in A (but ignore mux-expands
specified in B), filter. The difference is simply that in case 1 you
have no mux-expands in A (a mux-expand can also be given on the command
line, BTW).
3) command line switch --mux=auto or just --mux: take default values
from B and Z, expand mux-choices specified in A and B via mux-expand
4) command line switch --mux=all: take default values from B and Z,
apply mux-expand to all choices, filter out.
Yep, the possibility to multiplex on cmdline was intended :-)
######################################################################
Lucas also pointed out, that I didn't explain the hooks sufficiently. So
how the params.get() should look like:
def get(key, default):
raw_value = super(Params, self).get(key, default)
if key in self.hooks:
return self.hooks[key](raw_value, default)
else:
return raw_value
where hook could eg. means:
def arch_hook(raw_value, default):
# params.get("/hw/arch")
if not raw_value:
if default:
return default
else:
return arch.get_supported_archs()[0]
elif raw_value not in arch.get_supported_archs():
if default:
return default
else:
raise TestNAError("Architecture ... not supported")
I think actually it should always raise TestNAError in this second case,
giving the following very simple code for arch_hook:
value = raw_value or default or arch.get_supported_archs()[0]
if value not in arch.get_supported_archs():
raise TestNAError("Architecture ... not supported")
return value
However, I think it is important to have a declarative way to describe
the muxing and the defaults. A declarative syntax makes it possible to
convert muxing constraints to Boolean formulas, and hence enumerate the
results with binary decision diagrams (BDDs). In turn, this drops all
the complicated optimization logic of the cartesian config parser.
Now that I thought about it more, I recalled another idea from the old
thread, which is automatic expansion of filtered variants. In this idea
you'd have two variants of mux-only and mux-no:
- mux-requires-only and mux-requires-no, mostly for use in B and Z, do
nothing special
- mux-only and mux-no, mostly for use in A, automatically expand the
choices that are mentioned. In other words,
- !mux-only { '/os': 'linux' }
is the same as
- !mux-expand '/os'
- !mux-requires-only { '/os': 'linux' }
The idea is that in A you'd say
- !mux-no { '/hw/net/backend': 'vhost' }
meaning you want to test all backends except vhost. In B you'd say instead
- !mux-requires-no { '/hw/net/backend': 'user' }
meaning you want to skip this test, but not override the choice of the
user. It's also possible to do this magically (i.e. mux-only in A
always implies mux-expand) and avoid proliferation of tags. Still, the
difference in behavior should probably be taken into account.
Does this let you build arbitrary DNF (disjunctive normal form)
formulas? You can do ANDs of terms like
P | Q | R ~(P | Q | R)
You can only have direct and negated variables in the same formula if
you replicate the mux-group/mux-choice structure in A; this is quite
unwieldy. In the cartesian config this is provided by:
Linux:
only virtio
Windows:
only rtl8139
where Linux and Windows will match anywhere in the output tuple. What
we can do then is add a !mux-if tag:
- !mux-if { '/os': 'linux' }
- !mux-only { '/hw/net/nic_model': 'virtio' }
- !mux-if { '/os': 'windows' }
- !mux-only { '/hw/net/nic_model': 'rtl8139' }
This translates to the following Boolean formula
(os == linux => hw.net.nic_model == virtio) &&
(os == windows => hw.net.nic_model == rtl8139)
where I've written '/os/distro' more easily as 'os.distro' (probably
it's a good idea to write it that way in mux-{if,only,no} as well).
Rewriting the implication A => B to ~A | B:
(os != linux || hw.net.nic_model == virtio) &&
(os != windows || hw.net.nic_model == rtl8139)
and adding conditions to govern expansion of the choices:
(os != linux || hw.net.nic_model == virtio) &&
(os != windows || hw.net.nic_model == rtl8139) &&
expand(hw.net.nic_model)
Note that os only appears under a mux-if, so it is *not* expanded.
So, how are the mux directives translated to SAT formulas? Pretty
easily, I might say.
A choice with N possibilities is mapped to N+1 SAT variables, N for the
possibilities and 1 for "should this choice be expanded". So in the
case above there are six SAT variables:
expand(os)
os == linux
os == windows
expand(hw.net.nic_model)
hw.net.nic_model == virtio
hw.net.nic_model == rtl8139
Let's first define a "prefix" variable. It represents the !mux-if and
!mux-choice above the currently parsed element of the tree. So in the
case above, the prefix of the first !mux-if is "os == linux".
Similarly, when defining
- !mux-choice os:
- linux:
- !mux-choice distro:
- fedora:
- !mux-choice version:
- 13:
- 14:
- debian:
- !mux-choice version:
- squeeze:
- jessie:
- rhel:
the prefix while parsing fedora versions is "os == linux && os.distro ==
fedora"; the prefix while parsing debian versions is "os == linux &&
os.distro == debian"; and so on.
We'll use the negated prefix often, in order to build ORs instead of
implications. It is derived simply with De Morgan laws. The negation of
"os == linux && os.distro == fedora"
is
"os != linux || os.distro != fedora"
i.e. in terms of the variables we have defined above:
"!(os == linux) || !(os.distro == fedora)".
The negated prefix will be written prefix'.
With this in mind, here is how you do the conversion to SAT.
- !mux-choice name
A:
- mux-default: True
...
B:
...
C:
...
Your variables are
path.name == A
path.name == B
path.name == C
expand(path.name)
!expand(path.name)
which I'll write shortly as pA, pB, pC, pE, pE'. You first need
N(N-1)/2 terms to express mutual exclusion, and N-1 terms to express the
defaults. On the left they're written with implications, on the right
with disjunctions:
pA => ~pB ::: ~pA | ~pB (mutual exclusion)
pA => ~pC ::: ~pA | ~pC
pB => ~pC ::: ~pB | ~pC
~pE => ~pB ::: pE | ~pB (default)
~pE => ~pC ::: pE | ~pC
In addition, as mentioned above, the prefix is modified during the
parse. "& pA" is added to the prefix (and "| ~pA" to the negated
prefix) while parsing under "- A:", and so on.
- !mux-expand name
This is a simple term (left = implication, right = disjunction):
prefix => expand(path.name) ::: prefix' | expand(path.name)
This forces pE to be true and pE' to be false.
- !mux-if X
During the parse, X is added to the prefix, and ~X is added to the
negated prefix. No terms are emitted.
- !mux-requires-only { 'X': 'Y', 'U': 'V' }
Let's write the "X==Y" variable as pXY and the "U==V" variable as pUV.
You have the following termv (again, left uses arbitrary Boolean
formulas while right uses disjunctions only):
prefix => pXY | pUV ::: prefix' | pXY | pUV
- !mux-requires-no { 'X': 'Y', 'U': 'V' }
In this case every member of the map produces a separate term:
prefix => !pXY ::: prefix' | !pXY
prefix => !pUV ::: prefix' | !pUV
In order to produce the result of the multiplexing, you unfortunately
cannot just find all solutions to the SAT problem. This is because a
solution with pE = true will always be present; all the above does is
enforce no solution exists with pE = false when a choice is expanded.
There is thus a lot of redundancy in the solution computed so far.
Luckily, the BDD will represent cheaply the redundant solution _and_
will provide a way to easily cull the invalid solutions. To do so, we
can perform the following steps:
- ensure that, in the BDD, the expand(foo) variable lies above the
foo==X variables
- when visiting the BDD, if the 0 branch of expand(foo) has a solution,
skip visiting the 1 branch altogether.
Paolo
Oh my... Thank you for this elaborate. I'm going to need a board and
couple of hours/days to unparse and understand it.
Anyway the original idea behind the these hooks was not to make choices,
or cut down the tree. Original idea was just to create full tree (or
just defaults), execute the test and let it fail in case the dependency
is not met. Your solution (if I understand it correctly) goes further
and removes these tests from multiplexation. On the other hand, I don't
yet understand, how it works :-D
My approach was to execute the hooks in case user/test/framework asks
for the param while executing the test. Therefor the `None` architecture
makes sense, because it's just not set yet and it'll automatically
choose the right one. The same way would work the network for windows
vs. linux guests. You won't specify the default, but once the
framework/test asks for params.get('/virt/hw/nic_model') it'd execute hook:
def nic_hook(raw_value, default):
if raw_value is None:
return net.get_supported_nics()[0]
elif raw_value in net.get_supported_nics():
return raw_value
elif default:
return default
else:
raise TestNAError("Unable to match nic_model...")
Where the nic.get_supported_nics() would use params["/virt/os/distro"].
The benefit is, that on complex trees you'd not need to check all hooks,
only the ones which applies for given test. The cons against your idea
(if I understood it correctly) is, that your version would remove
non-suitable variants during multiplexation, my version would execute
the test up to the point where it gets to the params.get(trouble_maker)
call (where it would generate the TestNAError).
So correct me if I'm wrong, but my impression is that for bigger trees
with lots of irrelevant hooks your variant can be actually slower. For
small trees without variants, they should be probably the same speed and
for big trees with relevant hooks yours should be faster (and generates
smaller report as it'd skip irrelevant tests).
Anyway please give me couple of days to actually learn about the magic
you used, than I might completely change my opinion. In this email I
just wanted to clarify my original thought...
Regards,
Lukáš
Similarly you can handle -device support, default drive format, ...
Anyway I'll take a deeper look at the mux domains you proposed, it's
named version of what I had in mind (and better supported in yaml).
Thank you for your response,
Lukáš
I'm not 100% sure this is valid YAML, but should be close.
Thanks,
Paolo
_______________________________________________
Virt-test-devel mailing list
[email protected]
https://www.redhat.com/mailman/listinfo/virt-test-devel
_______________________________________________
Virt-test-devel mailing list
[email protected]
https://www.redhat.com/mailman/listinfo/virt-test-devel
_______________________________________________
Virt-test-devel mailing list
[email protected]
https://www.redhat.com/mailman/listinfo/virt-test-devel