Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(19)

Side by Side Diff: scripts/slave/recipe_configs_util.py

Issue 23889036: Refactor the way that TestApi works so that it is actually useful. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Yeeeeaaaahhhhhh! Created 7 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 # Copyright 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Recipe Configuration Meta DSL.
6
7 This module contains, essentially, a DSL for writing composable configurations.
8 You start by defining a schema which describes how your configuration blobs will
9 be structured, and what data they can contain. For example:
10
11 FakeSchema = lambda main_val=True, mode='Happy': ConfigGroup(
12 config_group = ConfigGroup(
13 item_a = SimpleConfig(int),
14 item_b = DictConfig(),
15 ),
16 extra_setting = SetConfig(str),
17
18 MAIN_DETERMINANT = StaticConfig(main_val),
19 CONFIG_MODE = StaticConfig(mode),
20 )
21
22 In short, a 'schema' is a callable which can take zero arguments (it can contain
23 default arguments as well, for setting defaults, tweaking the schema, etc.), and
24 returning a ConfigGroup.
25
26 Every type used in the schema derives from ConfigBase. It has the general
27 characteristics that it's a fixed-type container. It tends to impersonate the
28 data type that it stores (so you can manipulate the config objects like normal
29 python data), but also provides type checking and type conversion assistence
30 (so you can easily render your configurations to JSON).
31
32 Once you have your schema, you define some testing data:
33 TEST_MAP = {
34 'MAIN_DETERMINANT': (True, False),
35 'CONFIG_MODE': ('Happy', 'Sad'),
36 }
37 TEST_NAME_FORMAT = '%(MAIN_DETERMINANT)s-%(CONFIG_MODE)s'
38
39 The test map tells the test harness what parameters it should instantiate the
40 schema with, and what values those parameters should take. The test harness will
41 generate all possible permutations of input parameters, and will save them to
42 disk.
43
44 The test format is a string format (or a function taking a dictionary of
45 variable assignments) which will be used to name the test files
46 and test cases for this configuration.
47
48 Once you have all that, you can create a configuration context:
49
50 config_ctx = config_item_context(FakeSchema, TEST_MAP, TEST_NAME_FORMAT)
51
52 config_ctx is a python decorator which you can use to create composable
53 configuration functions. For example:
54
55 @config_ctx()
56 def cool(c):
57 if c.CONFIG_MODE == 'Happy':
58 c.config_group.item_a = 100
59 else:
60 c.config_group.item_a = -100
61
62 @config_ctx()
63 def gnarly(c):
64 c.extra_setting = 'gnarly!'
65
66 @config_ctx(includes=('cool', 'gnarly'))
67 def combo(c):
68 if c.MAIN_DETERMINANT:
69 c.config_group.item_b['nickname'] = 'purple'
70 c.extra_setting += ' cows!'
71 else:
72 c.config_group.item_b['nickname'] = 'sad times'
73
74 If I now call:
75
76 combo()
77
78 I will get back a configuraton object whose schema is FakeSchema, and whose
79 data is the accumulation of cool(), gnarly(), and combo(). I can continue to
80 manipulate this configuraton object, use its data, or render it to json.
81
82 Using this system should allow you to create rich, composible,
83 modular configurations. See the documentation on config_item_context and the
84 BaseConfig derivatives for more info.
85 """
86
87 import collections
88 import functools
89 import types
90
91 class BadConf(Exception):
92 pass
93
94 def config_item_context(CONFIG_SCHEMA, VAR_TEST_MAP, TEST_NAME_FORMAT,
95 TEST_FILE_FORMAT=None):
96 """Create a configuration context.
97
98 Args:
99 CONFIG_SCHEMA: This is a function which can take a minimum of zero arguments
100 and returns an instance of BaseConfig. This BaseConfig
101 defines the schema for all configuration objects manipulated
102 in this context.
103 VAR_TEST_MAP: A dict mapping arg_name to an iterable of values. This
104 provides the test harness with sufficient information to
105 generate all possible permutations of inputs for the
106 CONFIG_SCHEMA function.
107 TEST_NAME_FORMAT: A string format (or function) for naming tests and test
108 expectation files. It will be formatted/called with a
109 dictionary of arg_name to value (using arg_names and
110 values generated from VAR_TEST_MAP)
111 TEST_FILE_FORMAT: Similar to TEST_NAME_FORMAT, but for test files. Defaults
112 to TEST_NAME_FORMAT.
113
114 Returns a config_ctx decorator for this context.
115 """
116
117 def config_ctx(group=None, includes=None, deps=None, no_test=False,
118 is_root=False):
119 """
120 A decorator for functions which modify a given schema of configs.
121 Examples continue using the schema and config_items defined in the module
122 docstring.
123
124 This decorator provides a series of related functions:
125 * Any function decorated with this will be registered into this config
126 context by __name__. This enables some of the other following features
127 to work.
128 * Alters the signature of the function so that it can recieve an extra
129 parameter 'final'. See the documentation for final on inner().
130 * Provides various convenience and error checking facilities.
131 * In particular, this decorator will prevent you from calling the same
132 config_ctx on a given config blob more than once (with the exception
133 of setting final=False. See inner())
134
135 Args:
136 group(str) - Using this decorator with the `group' argument causes the
137 decorated function to be a member of that group. Members of a group are
138 mutually exclusive on the same configuration blob. For example, only
139 one of these two functions could be applied to the config blob c:
140 @config_ctx(group='a')
141 def bob(c):
142 c.extra_setting = "bob mode"
143
144 @config_ctx(group='a')
145 def bill(c):
146 c.extra_setting = "bill mode"
147
148 includes(iterable(str)) - Any config items named in the includes list will
149 be run against the config blob before the decorated function can modify
150 it. If an inclusion is already applied to the config blob, it's skipped
151 without applying/raising BadConf. Example:
152 @config_ctx(includes=('bob', 'cool'))
153 def charlie(c):
154 c.config_group.item_b = 25
155 The result of this config_ctx (assuming default values for the schema)
156 would be:
157 {'config_group': { 'item_a': 100, 'item_b': 25 },
158 'extra_setting': 'gnarly!'}
159
160 deps(iterable(str)) - One or more groups which must be satisfied before
161 this config_ctx can be applied to a config_blob. If you invoke
162 a config_ctx on a blob without having all of its deps satisfied,
163 you'll get a BadConf exception.
164
165 no_test(bool) - If set to True, then this config_ctx will be skipped by
166 the test harness. This defaults to (False or bool(deps)), since
167 config_items with deps will never be satisfiable as the first
168 config_ctx applied to a blob.
169
170 is_root(bool) - If set to True on an item, this item will become the
171 'basis' item for all other configurations in this group. That means that
172 it will be implicitly included in all other config_items. There may only
173 ever be one root item.
174
175 Additionally, the test harness uses the root item to probe for invalid
176 configuration combinations by running the root item first (if there is
177 one), and skipping the configuration combination if the root config
178 item throws BadConf.
179
180 Returns a new decorated version of this function (see inner()).
181 """
182 def decorator(f):
183 name = f.__name__
184 @functools.wraps(f)
185 def inner(config=None, final=True, optional=False, **kwargs):
186 """This is the function which is returned from the config_ctx
187 decorator.
188
189 It applies all of the logic mentioned in the config_ctx docstring
190 above, and alters the function signature slightly.
191
192 Args:
193 config - The config blob that we intend to manipulate. This is passed
194 through to the function after checking deps and including includes.
195 After the function manipulates it, it is automatically returned.
196
197 final(bool) - Set to True by default, this will record the application
198 of this config_ctx to `config', which will prevent the config_ctx
199 from being applied to `config' again. It also is used to see if the
200 config blob satisfies deps for subsequent config_ctx applications
201 (i.e. in order for a config_ctx to satisfy a dependency, it must
202 be applied with final=True).
203
204 This is useful to apply default values while allowing the config to
205 later override those values.
206
207 However, it's best if each config_ctx is final, because then you
208 can implement the config items with less error checking, since you
209 know that the item may only be applied once. For example, if your
210 item appends something to a list, but is called with final=False,
211 you'll have to make sure that you don't append the item twice, etc.
212
213 **kwargs - Passed through to the decorated function without harm.
214
215 Returns config and ignores the return value of the decorated function.
216 """
217 if config is None:
218 config = config_ctx.CONFIG_SCHEMA()
219 assert isinstance(config, ConfigGroup)
220 inclusions = config._inclusions # pylint: disable=W0212
221
222 # inner.IS_ROOT will be True or False at the time of invocation.
223 if (config_ctx.ROOT_CONFIG_ITEM and not inner.IS_ROOT and
224 config_ctx.ROOT_CONFIG_ITEM.__name__ not in inclusions):
225 config_ctx.ROOT_CONFIG_ITEM(config)
226
227 if name in inclusions:
228 if optional:
229 return config
230 raise BadConf('config_ctx "%s" is already in this config "%s"' %
231 (name, config.as_jsonish(include_hidden=True)))
232 if final:
233 inclusions.add(name)
234
235 for include in (includes or []):
236 if include in inclusions:
237 continue
238 try:
239 config_ctx.CONFIG_ITEMS[include](config)
240 except BadConf, e:
241 raise BadConf('config "%s" includes "%s", but [%s]' %
242 (name, include, e))
243
244 # deps are a list of group names. All groups must be represented
245 # in config already.
246 for dep_group in (deps or []):
247 if not (inclusions & config_ctx.MUTEX_GROUPS[dep_group]):
248 raise BadConf('dep group "%s" is unfulfilled for "%s"' %
249 (dep_group, name))
250
251 if group:
252 overlap = inclusions & config_ctx.MUTEX_GROUPS[group]
253 overlap.discard(name)
254 if overlap:
255 raise BadConf('"%s" is a member of group "%s", but %s already ran' %
256 (name, group, tuple(overlap)))
257
258 ret = f(config, **kwargs)
259 assert ret is None, 'Got return value (%s) from "%s"?' % (ret, name)
260
261 return config
262
263 assert name not in config_ctx.CONFIG_ITEMS
264 config_ctx.CONFIG_ITEMS[name] = inner
265 if group:
266 config_ctx.MUTEX_GROUPS.setdefault(group, set()).add(name)
267 inner.IS_ROOT = is_root
268 if is_root:
269 assert not config_ctx.ROOT_CONFIG_ITEM, (
270 'may only have one root config_ctx!')
271 config_ctx.ROOT_CONFIG_ITEM = inner
272 inner.IS_ROOT = True
273 inner.NO_TEST = no_test or bool(deps)
274 return inner
275 return decorator
276
277 # Internal state and testing data
278 config_ctx.I_AM_A_CONFIG_CTX = True
279 config_ctx.CONFIG_ITEMS = {}
280 config_ctx.MUTEX_GROUPS = {}
281 config_ctx.CONFIG_SCHEMA = CONFIG_SCHEMA
282 config_ctx.ROOT_CONFIG_ITEM = None
283 config_ctx.VAR_TEST_MAP = VAR_TEST_MAP
284
285 def formatter(obj, ext=None):
286 '''Converts format obj to a function taking var assignments.
287
288 Args:
289 obj (str or fn(assignments)): If obj is a str, it will be % formatted
290 with assignments (which is a dict of variables from VAR_TEST_MAP).
291 Otherwise obj will be invoked with assignments, and expected to return
292 a fully-rendered string.
293 ext (None or str): Optionally specify an extension to enforce on the
294 format. This enforcement occurs after obj is finalized to a string. If
295 the string doesn't end with ext, it will be appended.
296 '''
297 def inner(var_assignments):
298 ret = ''
299 if isinstance(obj, basestring):
300 ret = obj % var_assignments
301 else:
302 ret = obj(var_assignments)
303 if ext and not ret.endswith(ext):
304 ret += ext
305 return ret
306 return inner
307 config_ctx.TEST_NAME_FORMAT = formatter(TEST_NAME_FORMAT)
308 config_ctx.TEST_FILE_FORMAT = formatter(
309 (TEST_FILE_FORMAT or TEST_NAME_FORMAT), ext='.json')
310 return config_ctx
311
312
313 class ConfigBase(object):
314 """This is the root interface for all config schema types."""
315
316 def __init__(self, hidden=False):
317 """
318 Args:
319 hidden - If set to True, this object will be excluded from printing when
320 the config blob is rendered with ConfigGroup.as_jsonish(). You still
321 have full read/write access to this blob otherwise though.
322 """
323 # work around subclasses which override __setattr__
324 object.__setattr__(self, '_hidden', hidden)
325 object.__setattr__(self, '_inclusions', set())
326
327 def get_val(self):
328 """Gets the native value of this config object."""
329 return self
330
331 def set_val(self, val):
332 """Resets the value of this config object using data in val."""
333 raise NotImplementedError
334
335 def reset(self):
336 """Resets the value of this config object to it's initial state."""
337 raise NotImplementedError
338
339 def as_jsonish(self, include_hidden=False):
340 """Returns the value of this config object as simple types."""
341 raise NotImplementedError
342
343 def complete(self):
344 """Returns True iff this configuraton blob is fully viable."""
345 raise NotImplementedError
346
347
348 class ConfigGroup(ConfigBase):
349 """Allows you to provide hierarchy to a configuration schema.
350
351 Example usage:
352 config_blob = ConfigGroup(
353 some_item = SimpleConfig(str),
354 group = ConfigGroup(
355 numbahs = SetConfig(int),
356 ),
357 )
358 config_blob.some_item = "hello"
359 config_blob.group.numbahs.update(range(10))
360 """
361
362 def __init__(self, hidden=False, **type_map):
363 """Expects type_map to be {python_name -> ConfigBase} instance."""
364 super(ConfigGroup, self).__init__(hidden)
365 assert type_map, 'A ConfigGroup with no type_map is meaningless.'
366
367 object.__setattr__(self, '_type_map', type_map)
368 for name, typeval in self._type_map.iteritems():
369 assert isinstance(typeval, ConfigBase)
370 object.__setattr__(self, name, typeval)
371
372 def __getattribute__(self, name):
373 obj = object.__getattribute__(self, name)
374 if isinstance(obj, ConfigBase):
375 return obj.get_val()
376 else:
377 return obj
378
379 def __setattr__(self, name, val):
380 obj = object.__getattribute__(self, name)
381 assert isinstance(obj, ConfigBase)
382 obj.set_val(val)
383
384 def __delattr__(self, name):
385 obj = object.__getattribute__(self, name)
386 assert isinstance(obj, ConfigBase)
387 obj.reset()
388
389 def set_val(self, val):
390 if isinstance(val, ConfigBase):
391 val = val.as_jsonish(include_hidden=True)
392 assert isinstance(val, dict)
393 for name, config_obj in self._type_map.iteritems():
394 if name in val:
395 config_obj.set_val(val.pop())
396 assert not val, "Got extra keys while setting ConfigGroup: %s" % val
397
398 def as_jsonish(self, include_hidden=False):
399 return dict(
400 (n, v.as_jsonish(include_hidden)) for n, v in self._type_map.iteritems()
401 if (include_hidden or not v._hidden)) # pylint: disable=W0212
402
403 def reset(self):
404 for v in self._type_map.values():
405 v.reset()
406
407 def complete(self):
408 return all(v.complete() for v in self._type_map.values())
409
410
411 class ConfigList(ConfigBase, collections.MutableSequence):
412 """Allows you to provide an ordered repetition to a configuration schema.
413
414 Example usage:
415 config_blob = ConfigGroup(
416 some_items = ConfigList(
417 ConfigGroup(
418 herp = SimpleConfig(int),
419 derp = SimpleConfig(str)
420 )
421 )
422 )
423 config_blob.some_items.append({'herp': 1})
424 config_blob.some_items[0].derp = 'bob'
425 """
426
427 def __init__(self, item_schema, hidden=False):
428 """
429 Args:
430 item_schema: The schema of each object. Should be a function which returns
431 an instance of ConfigGroup.
432 """
433 super(ConfigList, self).__init__(hidden=hidden)
434 assert isinstance(item_schema, types.FunctionType)
435 assert isinstance(item_schema(), ConfigGroup)
436 self.item_schema = item_schema
437 self.data = []
438
439 def __getitem__(self, index):
440 return self.data.__getitem__(index)
441
442 def __setitem__(self, index, value):
443 datum = self.item_schema()
444 datum.set_val(value)
445 return self.data.__setitem__(index, datum)
446
447 def __delitem__(self, index):
448 return self.data.__delitem__(index)
449
450 def __len__(self):
451 return len(self.data)
452
453 def insert(self, index, value):
454 datum = self.item_schema()
455 datum.set_val(value)
456 return self.data.insert(index, datum)
457
458 def add(self):
459 self.append({})
460 return self[-1]
461
462 def reset(self):
463 self.data = []
464
465 def complete(self):
466 return all(i.complete() for i in self.data)
467
468 def set_val(self, data):
469 if isinstance(data, ConfigList):
470 data = data.as_jsonish(include_hidden=True)
471 assert isinstance(data, list)
472 self.reset()
473 for item in data:
474 self.append(item)
475
476 def as_jsonish(self, include_hidden=False):
477 return [i.as_jsonish(include_hidden) for i in self.data
478 if (include_hidden or not i._hidden)] # pylint: disable=W0212
479
480
481 class DictConfig(ConfigBase, collections.MutableMapping):
482 """Provides a semi-homogenous dict()-like configuration object."""
483
484 def __init__(self, item_fn=lambda i: i, jsonish_fn=dict, value_type=None,
485 hidden=False):
486 """
487 Args:
488 item_fn - A function which renders (k, v) pairs to input items for
489 jsonish_fn. Defaults to the identity function.
490 jsonish_fn - A function which renders a list of outputs of item_fn to a
491 JSON-compatiple python datatype. Defaults to dict().
492 value_type - A type object used for constraining/validating the values
493 assigned to this dictionary.
494 hidden - See ConfigBase.
495 """
496 super(DictConfig, self).__init__(hidden)
497 self.value_type = value_type
498 self.item_fn = item_fn
499 self.jsonish_fn = jsonish_fn
500 self.data = {}
501
502 def __getitem__(self, k):
503 return self.data.__getitem__(k)
504
505 def __setitem__(self, k, v):
506 if self.value_type:
507 assert isinstance(v, self.value_type)
508 return self.data.__setitem__(k, v)
509
510 def __delitem__(self, k):
511 return self.data.__delitem__(k)
512
513 def __iter__(self):
514 return iter(self.data)
515
516 def __len__(self):
517 return len(self.data)
518
519 def set_val(self, val):
520 if isinstance(val, DictConfig):
521 val = val.data
522 assert isinstance(val, dict)
523 assert all(isinstance(v, self.value_type) for v in val.itervalues())
524 self.data = val
525
526 def as_jsonish(self, _include_hidden=None):
527 return self.jsonish_fn(map(
528 self.item_fn, sorted(self.data.iteritems(), key=lambda x: x[0])))
529
530 def reset(self):
531 self.data.clear()
532
533 def complete(self):
534 return True
535
536
537 class ListConfig(ConfigBase, collections.MutableSequence):
538 """Provides a semi-homogenous list()-like configuration object."""
539
540 def __init__(self, inner_type, jsonish_fn=list, hidden=False):
541 """
542 Args:
543 inner_type - The type of data contained in this set, e.g. str, int, ...
544 Can also be a tuple of types to allow more than one type.
545 jsonish_fn - A function used to reduce the list() to a JSON-compatible
546 python datatype. Defaults to list().
547 hidden - See ConfigBase.
548 """
549 super(ListConfig, self).__init__(hidden)
550 self.inner_type = inner_type
551 self.jsonish_fn = jsonish_fn
552 self.data = []
553
554 def __getitem__(self, index):
555 return self.data[index]
556
557 def __setitem__(self, index, value):
558 assert isinstance(value, self.inner_type)
559 self.data[index] = value
560
561 def __delitem__(self, index):
562 del self.data
563
564 def __len__(self):
565 return len(self.data)
566
567 def __radd__(self, other):
568 if not isinstance(other, list):
569 other = list(other)
570 return other + self.data
571
572 def insert(self, index, value):
573 assert isinstance(value, self.inner_type)
574 self.data.insert(index, value)
575
576 def set_val(self, val):
577 assert all(isinstance(v, self.inner_type) for v in val)
578 self.data = list(val)
579
580 def as_jsonish(self, _include_hidden=None):
581 return self.jsonish_fn(self.data)
582
583 def reset(self):
584 self.data = []
585
586 def complete(self):
587 return True
588
589
590 class SetConfig(ConfigBase, collections.MutableSet):
591 """Provides a semi-homogenous set()-like configuration object."""
592
593 def __init__(self, inner_type, jsonish_fn=list, hidden=False):
594 """
595 Args:
596 inner_type - The type of data contained in this set, e.g. str, int, ...
597 Can also be a tuple of types to allow more than one type.
598 jsonish_fn - A function used to reduce the set() to a JSON-compatible
599 python datatype. Defaults to list().
600 hidden - See ConfigBase.
601 """
602 super(SetConfig, self).__init__(hidden)
603 self.inner_type = inner_type
604 self.jsonish_fn = jsonish_fn
605 self.data = set()
606
607 def __contains__(self, val):
608 return val in self.data
609
610 def __iter__(self):
611 return iter(self.data)
612
613 def __len__(self):
614 return len(self.data)
615
616 def add(self, value):
617 assert isinstance(value, self.inner_type)
618 self.data.add(value)
619
620 def discard(self, value):
621 self.data.discard(value)
622
623 def set_val(self, val):
624 assert all(isinstance(v, self.inner_type) for v in val)
625 self.data = set(val)
626
627 def as_jsonish(self, _include_hidden=None):
628 return self.jsonish_fn(sorted(self.data))
629
630 def reset(self):
631 self.data = set()
632
633 def complete(self):
634 return True
635
636
637 class SimpleConfig(ConfigBase):
638 """Provides a configuration object which holds a single 'simple' type."""
639
640 def __init__(self, inner_type, jsonish_fn=lambda x: x, empty_val=None,
641 required=True, hidden=False):
642 """
643 Args:
644 inner_type - The type of data contained in this object, e.g. str, int, ...
645 Can also be a tuple of types to allow more than one type.
646 jsonish_fn - A function used to reduce the data to a JSON-compatible
647 python datatype. Default is the identity function.
648 emtpy_val - The value to use when initializing this object or when calling
649 reset().
650 required(bool) - True iff this config item is required to have a
651 non-empty_val in order for it to be considered complete().
652 hidden - See ConfigBase.
653 """
654 super(SimpleConfig, self).__init__(hidden)
655 self.inner_type = inner_type
656 self.jsonish_fn = jsonish_fn
657 self.empty_val = empty_val
658 self.data = empty_val
659 self.required = required
660
661 def get_val(self):
662 return self.data
663
664 def set_val(self, val):
665 if isinstance(val, SimpleConfig):
666 val = val.data
667 assert val is self.empty_val or isinstance(val, self.inner_type)
668 self.data = val
669
670 def as_jsonish(self, _include_hidden=None):
671 return self.jsonish_fn(self.data)
672
673 def reset(self):
674 self.data = self.empty_val
675
676 def complete(self):
677 return not self.required or self.data is not self.empty_val
678
679
680 class StaticConfig(ConfigBase):
681 """Holds a single, hidden, immutible data object.
682
683 This is very useful for holding the 'input' configuration values (i.e. those
684 which are in your VAR_TEST_MAP).
685 """
686
687 def __init__(self, value, hidden=True):
688 super(StaticConfig, self).__init__(hidden=hidden)
689 # Attempt to hash the value, which will ensure that it's immutable all the
690 # way down :).
691 hash(value)
692 self.data = value
693
694 def get_val(self):
695 return self.data
696
697 def set_val(self, val):
698 assert False
699
700 def as_jsonish(self, _include_hidden=None):
701 return self.data
702
703 def reset(self):
704 assert False
705
706 def complete(self):
707 return True
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698