Skip to content

Commit fea2273

Browse files
authored
Result by keys (#754)
* Fix autodetection of dependencies when in a sub-repo. * Some results improvements - Added '--by-key' and '--by-key-compat' to 'pav results' this prints a subtable of results in a pretty way, and merges results across that subtable for multiple tests. - Added optional arguments to expression functions. Wrap specs in the new Opt() class to use. - Added high_pass_filter and low_pass_filter expression functions. * Update base.py * Update core.py * Update base.py * Style issues. * Fixed unittest issues.
1 parent 55aaa65 commit fea2273

File tree

7 files changed

+259
-44
lines changed

7 files changed

+259
-44
lines changed

lib/pavilion/commands/result.py

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Print the test results for the given test/suite."""
22

3+
from collections import defaultdict
34
import datetime
45
import errno
56
import io
@@ -42,6 +43,16 @@ def _setup_arguments(self, parser):
4243
action="store_true", default=False,
4344
help="Give the results in json."
4445
)
46+
parser.add_argument(
47+
"--by-key", type=str, default='',
48+
help="Show the data in the given results key instead of the regular results. \n"
49+
"Such keys must contain a dictionary of dictionaries. Use the `--by-key-compat`\n"
50+
"argument to find out which keys are compatible. Results from all matched \n"
51+
"tests are combined (duplicates are ignored).\n"
52+
"Example `pav results --by-key=per_file`.")
53+
parser.add_argument(
54+
"--by-key-compat", action="store_true",
55+
help="List keys compatible with the '--by-key' argument.")
4556
group = parser.add_mutually_exclusive_group()
4657
group.add_argument(
4758
"-k", "--key", type=str, default='',
@@ -94,8 +105,6 @@ def _setup_arguments(self, parser):
94105
def run(self, pav_cfg, args):
95106
"""Print the test results in a variety of formats."""
96107

97-
fields = self.key_fields(args)
98-
99108
test_paths = cmd_utils.arg_filtered_tests(pav_cfg, args,
100109
verbose=self.errfile).paths
101110
tests = cmd_utils.get_tests_by_paths(pav_cfg, test_paths, self.errfile)
@@ -113,14 +122,66 @@ def run(self, pav_cfg, args):
113122
serieses = ",".join(
114123
set([test.series for test in tests if test.series is not None]))
115124
results = result_utils.get_results(pav_cfg, tests)
116-
flat_results = []
117-
all_passed = True
118-
for rslt in results:
119-
flat_results.append(utils.flatten_dictionary(rslt))
120-
if rslt['result'] != TestRun.PASS:
121-
all_passed = False
122125

123-
field_info = {}
126+
if args.by_key_compat:
127+
compat_keys = set()
128+
for rslt in results:
129+
for key in rslt:
130+
if isinstance(rslt[key], dict):
131+
for subkey, val in rslt[key].items():
132+
if isinstance(val, dict):
133+
compat_keys.add(key)
134+
break
135+
136+
if 'var' in compat_keys:
137+
compat_keys.remove("var")
138+
139+
output.fprint(self.outfile, "Keys compatible with '--by-key'")
140+
for key in compat_keys:
141+
output.fprint(self.outfile, " ", key)
142+
143+
return 0
144+
145+
elif args.by_key:
146+
reorged_results = defaultdict(dict)
147+
fields = set()
148+
for rslt in results:
149+
subtable = rslt.get(args.by_key, None)
150+
if not isinstance(subtable, dict):
151+
continue
152+
for key, values in subtable.items():
153+
if not isinstance(values, dict):
154+
continue
155+
reorged_results[key].update(values)
156+
fields = fields.union(values.keys())
157+
158+
fields = ['--tag'] + sorted(fields)
159+
flat_results = []
160+
for key, values in reorged_results.items():
161+
values['--tag'] = key
162+
flat_results.append(values)
163+
164+
flat_results.sort(key=lambda val: val['--tag'])
165+
166+
field_info = {
167+
'--tag': {'title': ''},
168+
}
169+
170+
else:
171+
fields = self.key_fields(args)
172+
flat_results = []
173+
all_passed = True
174+
for rslt in results:
175+
flat_results.append(utils.flatten_dictionary(rslt))
176+
if rslt['result'] != TestRun.PASS:
177+
all_passed = False
178+
field_info = {
179+
'created': {'transform': output.get_relative_timestamp},
180+
'started': {'transform': output.get_relative_timestamp},
181+
'finished': {'transform': output.get_relative_timestamp},
182+
'duration': {'transform': output.format_duration},
183+
}
184+
124185

125186
if args.list_keys:
126187
flat_keys = result_utils.keylist(flat_results)
@@ -132,7 +193,6 @@ def run(self, pav_cfg, args):
132193
title_str=f"Available keys for specified tests in {serieses}."
133194

134195
output.draw_table(outfile=self.outfile,
135-
field_info=field_info,
136196
fields=fields,
137197
rows=flatter_keys,
138198
border=True,
@@ -161,13 +221,6 @@ def run(self, pav_cfg, args):
161221
else:
162222
flat_sorted_results = utils.sort_table(args.sort_by, flat_results)
163223

164-
field_info = {
165-
'created': {'transform': output.get_relative_timestamp},
166-
'started': {'transform': output.get_relative_timestamp},
167-
'finished': {'transform': output.get_relative_timestamp},
168-
'duration': {'transform': output.format_duration},
169-
}
170-
171224
title_str=f"Test Results: {serieses}."
172225
output.draw_table(
173226
outfile=self.outfile,

lib/pavilion/expression_functions/base.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import inspect
44
import logging
55
import re
6+
from typing import Any
67

78
from yapsy import IPlugin
89
from ..errors import FunctionPluginError
@@ -36,6 +37,18 @@ def num(val):
3637
raise RuntimeError("Invalid value '{}' given to num.".format(val))
3738

3839

40+
class Opt:
41+
"""An optional arg spec, the contained spec is checked if the value is given."""
42+
43+
def __init__(self, sub_spec: Any):
44+
"""
45+
:param sub_spec: The type of the argument spec to accept.
46+
Ex: Opt(int) or Opt([str])
47+
"""
48+
49+
self.sub_spec = sub_spec
50+
51+
3952
class FunctionPlugin(IPlugin.IPlugin):
4053
"""Plugin base class for math functions.
4154
@@ -51,6 +64,7 @@ class FunctionPlugin(IPlugin.IPlugin):
5164
str,
5265
bool,
5366
num,
67+
Opt,
5468
None
5569
)
5670

@@ -130,7 +144,6 @@ def _validate_arg_spec(self, arg):
130144
dicts, and types from self.VALID_SPEC_TYPES.
131145
132146
- Lists should contain one representative containing type.
133-
- Dicts should have at least one key-value pair (with string keys).
134147
- Dict specs don't have to contain every key the dict might have,
135148
just those that will be used.
136149
- Specs may be any structure of these types, as long
@@ -139,11 +152,15 @@ def _validate_arg_spec(self, arg):
139152
or bool. ints and floats are left alone, bools become
140153
ints, and strings become an int or a float if they can.
141154
- 'None' may be given as the type of contained items for lists
142-
or dicts, denoting that contained type doesn't matter.
155+
or dicts, denoting that contained type doesn't matter. Dict
156+
specs can similarly contain no items.
143157
:raises FunctionPluginError: On a bad arg spec.
144158
"""
145159

146-
if isinstance(arg, list):
160+
if isinstance(arg, Opt):
161+
self._validate_arg_spec(arg.sub_spec)
162+
163+
elif isinstance(arg, list):
147164
if len(arg) != 1:
148165
raise FunctionPluginError(
149166
"Invalid list spec argument. List arguments must contain "
@@ -153,12 +170,6 @@ def _validate_arg_spec(self, arg):
153170
self._validate_arg_spec(arg[0])
154171

155172
elif isinstance(arg, dict):
156-
if len(arg) == 0:
157-
raise FunctionPluginError(
158-
"Invalid dict spec argument. Dict arguments must contain "
159-
"at least one key-value pair. This had '{}'."
160-
.format(arg)
161-
)
162173
for key, sub_arg in arg.items():
163174
self._validate_arg_spec(sub_arg)
164175

@@ -182,7 +193,7 @@ def __call__(self, *args):
182193
"""Validate/convert the arguments and call the function."""
183194

184195
if self.arg_specs is not None:
185-
if len(args) != len(self.arg_specs):
196+
if len(args) > len(self.arg_specs):
186197
raise FunctionPluginError(
187198
"Invalid number of arguments defined for function {}. Got "
188199
"{}, but expected {}"
@@ -243,6 +254,8 @@ def _spec_to_desc(self, spec):
243254
return [self._spec_to_desc(spec[0])]
244255
elif isinstance(spec, dict):
245256
return {k: self._spec_to_desc(v) for k, v in spec.items()}
257+
elif isinstance(spec, Opt):
258+
return self._spec_to_desc(spec.sub_spec) + '?'
246259
elif spec is None:
247260
return 'Any'
248261
else:
@@ -258,6 +271,9 @@ def _validate_arg(self, arg, spec):
258271
:return: The validated, auto-converted argument.
259272
"""
260273

274+
if isinstance(spec, Opt):
275+
return self._validate_arg(arg, spec.sub_spec)
276+
261277
if isinstance(spec, list):
262278
if not isinstance(arg, list):
263279
raise FunctionPluginError(
@@ -290,13 +306,13 @@ def _validate_arg(self, arg, spec):
290306
.format(arg, key))
291307

292308
try:
293-
val_args[key] = self._validate_arg(arg[key], sub_spec)
309+
self._validate_arg(arg[key], sub_spec)
294310
except FunctionPluginError as err:
295311
raise FunctionPluginError(
296312
"Invalid dict argument '{}' for key '{}'"
297313
.format(arg[key], key), err)
298314

299-
return val_args
315+
return arg
300316

301317
if spec is None:
302318
# None denotes to leave the argument alone.

lib/pavilion/expression_functions/core.py

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import math
55
import random
66
import re
7-
from typing import List
7+
from typing import List, Dict, Union
88

9-
from .base import FunctionPlugin, num
9+
from .base import FunctionPlugin, num, Opt
1010
from ..errors import FunctionPluginError, FunctionArgError
1111

1212

@@ -253,18 +253,9 @@ def __init__(self):
253253

254254
super().__init__(
255255
name='keys',
256-
arg_specs=None,
256+
arg_specs=({},),
257257
)
258258

259-
signature = "keys(dict)"
260-
261-
def _validate_arg(self, arg, spec):
262-
if not isinstance(arg, dict):
263-
raise FunctionPluginError(
264-
"The dicts function only accepts dicts. Got {} of type {}."
265-
.format(arg, type(arg).__name__))
266-
return arg
267-
268259
@staticmethod
269260
def keys(arg):
270261
"""Return a (sorted) list of keys for the given dictionary."""
@@ -370,6 +361,93 @@ def sqrt(value: num):
370361
return value ** 0.5
371362

372363

364+
class HighPassFilter(CoreFunctionPlugin):
365+
"""Given the 'value_dict', return a new dictionary that contains only
366+
items that exceed 'limit'. For dicts of dicts, you must specify an item_key
367+
to check limit against.
368+
369+
Examples:
370+
Given dict 'data={a: 1, b: 2, c: 3, d: 4}',
371+
`high_pass_filter(data, 3)` would return a dict with
372+
the 'c' and 'd' keys removed.
373+
374+
Given dict 'data={foo: {a: 5}, bar: {a: 100}}, baz: {a: 20}}'
375+
`high_pass_filter(data, 20, 'a')` would return a dict containing
376+
only key 'foo' and its value/s."""
377+
378+
def __init__(self):
379+
super().__init__(
380+
'high_pass_filter',
381+
arg_specs=({}, num, Opt(str)))
382+
383+
@staticmethod
384+
def high_pass_filter(value_dict: Dict, limit: Union[int, float], item_key: str = None) -> Dict:
385+
"""Return only items > limit"""
386+
387+
new_dict = {}
388+
for key, values in value_dict.items():
389+
if isinstance(values, dict):
390+
if item_key is None:
391+
raise FunctionArgError("value_dict contained a dict, but no key was specified.")
392+
393+
value = values.get(item_key)
394+
else:
395+
if item_key is not None:
396+
raise FunctionArgError(
397+
"value_dict contained a non-dictionary, but a key was specified.")
398+
399+
value = values
400+
401+
if isinstance(value, (int, float, str)):
402+
value = num(value)
403+
else:
404+
continue
405+
406+
if value > limit:
407+
new_dict[key] = values
408+
409+
return new_dict
410+
411+
412+
class LowPassFilter(CoreFunctionPlugin):
413+
"""Given the 'value_dict', return a new dictionary that contains only
414+
items that are less than 'limit'. For dicts of dicts, you must specify
415+
a sub-key to check 'limit' against. See 'high_pass_filter' for examples."""
416+
417+
def __init__(self):
418+
super().__init__(
419+
'low_pass_filter',
420+
arg_specs=({}, num, Opt(str)))
421+
422+
@staticmethod
423+
def low_pass_filter(value_dict: Dict, limit: Union[int, float], item_key: str = None) -> Dict:
424+
"""Return only items > limit"""
425+
426+
new_dict = {}
427+
for key, values in value_dict.items():
428+
if isinstance(values, dict):
429+
if item_key is None:
430+
raise FunctionArgError("value_dict contained a dict, but no key was specified.")
431+
432+
value = values.get(item_key)
433+
else:
434+
if item_key is not None:
435+
raise FunctionArgError(
436+
"value_dict contained a non-dictionary, but a key was specified.")
437+
438+
value = values
439+
440+
if isinstance(value, (int, float, str)):
441+
value = num(value)
442+
else:
443+
continue
444+
445+
if value < limit:
446+
new_dict[key] = values
447+
448+
return new_dict
449+
450+
373451
class Range(CoreFunctionPlugin):
374452
"""Return a list of numbers from a..b, not inclusive of b."""
375453

@@ -390,6 +468,7 @@ def range(start, end):
390468

391469
return vals
392470

471+
393472
class Outliers(CoreFunctionPlugin):
394473
"""Calculate outliers given a list of values and a separate list
395474
of their associated names. The lists should be the same length, and

test/data/configs-rerun/tests/result_tests.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ permuted:
3131
result: 'world < 50 * {{var1}}'
3232
other: 'world * {{var1}} + {{var2}}'
3333

34+
35+
complex:
36+
result_evaluate:
37+
result: '0 == 0'

0 commit comments

Comments
 (0)