Source code

Revision control

Other Tools

1
from __future__ import print_function
2
import os
3
from six.moves.urllib.parse import urljoin, urlsplit
4
from collections import namedtuple, defaultdict, deque
5
from math import ceil
6
from six import iterkeys, itervalues, iteritems
7
8
from .wptmanifest import serialize
9
from .wptmanifest.node import (DataNode, ConditionalNode, BinaryExpressionNode,
10
BinaryOperatorNode, NumberNode, StringNode, VariableNode,
11
ValueNode, UnaryExpressionNode, UnaryOperatorNode,
12
ListNode)
13
from .wptmanifest.backends import conditional
14
from .wptmanifest.backends.conditional import ManifestItem
15
16
from . import expected
17
from . import expectedtree
18
19
"""Manifest structure used to update the expected results of a test
20
21
Each manifest file is represented by an ExpectedManifest that has one
22
or more TestNode children, one per test in the manifest. Each
23
TestNode has zero or more SubtestNode children, one for each known
24
subtest of the test.
25
26
In these representations, conditionals expressions in the manifest are
27
not evaluated upfront but stored as python functions to be evaluated
28
at runtime.
29
30
When a result for a test is to be updated set_result on the
31
[Sub]TestNode is called to store the new result, alongside the
32
existing conditional that result's run info matched, if any. Once all
33
new results are known, update is called to compute the new
34
set of results and conditionals. The AST of the underlying parsed manifest
35
is updated with the changes, and the result is serialised to a file.
36
"""
37
38
39
class ConditionError(Exception):
40
def __init__(self, cond=None):
41
self.cond = cond
42
43
44
class UpdateError(Exception):
45
pass
46
47
48
Value = namedtuple("Value", ["run_info", "value"])
49
50
51
def data_cls_getter(output_node, visited_node):
52
# visited_node is intentionally unused
53
if output_node is None:
54
return ExpectedManifest
55
elif isinstance(output_node, ExpectedManifest):
56
return TestNode
57
elif isinstance(output_node, TestNode):
58
return SubtestNode
59
else:
60
raise ValueError
61
62
63
class UpdateProperties(object):
64
def __init__(self, manifest, **kwargs):
65
self._manifest = manifest
66
self._classes = kwargs
67
68
def __getattr__(self, name):
69
if name in self._classes:
70
rv = self._classes[name](self._manifest)
71
setattr(self, name, rv)
72
return rv
73
raise AttributeError
74
75
def __contains__(self, name):
76
return name in self._classes
77
78
def __iter__(self):
79
for name in iterkeys(self._classes):
80
yield getattr(self, name)
81
82
83
class ExpectedManifest(ManifestItem):
84
def __init__(self, node, test_path, url_base, run_info_properties,
85
update_intermittent=False, remove_intermittent=False):
86
"""Object representing all the tests in a particular manifest
87
88
:param node: AST Node associated with this object. If this is None,
89
a new AST is created to associate with this manifest.
90
:param test_path: Path of the test file associated with this manifest.
91
:param url_base: Base url for serving the tests in this manifest.
92
:param run_info_properties: Tuple of ([property name],
93
{property_name: [dependent property]})
94
The first part lists run_info properties
95
that are always used in the update, the second
96
maps property names to additional properties that
97
can be considered if we already have a condition on
98
the key property e.g. {"foo": ["bar"]} means that
99
we consider making conditions on bar only after we
100
already made one on foo.
101
:param update_intermittent: When True, intermittent statuses will be recorded
102
as `expected` in the test metadata.
103
:param: remove_intermittent: When True, old intermittent statuses will be removed
104
if no longer intermittent. This is only relevant if
105
`update_intermittent` is also True, because if False,
106
the metadata will simply update one `expected`status.
107
"""
108
if node is None:
109
node = DataNode(None)
110
ManifestItem.__init__(self, node)
111
self.child_map = {}
112
self.test_path = test_path
113
self.url_base = url_base
114
assert self.url_base is not None
115
self._modified = False
116
self.run_info_properties = run_info_properties
117
self.update_intermittent = update_intermittent
118
self.remove_intermittent = remove_intermittent
119
self.update_properties = UpdateProperties(self, **{
120
"lsan": LsanUpdate,
121
"leak_object": LeakObjectUpdate,
122
"leak_threshold": LeakThresholdUpdate,
123
})
124
125
@property
126
def modified(self):
127
if self._modified:
128
return True
129
return any(item.modified for item in self.children)
130
131
@modified.setter
132
def modified(self, value):
133
self._modified = value
134
135
def append(self, child):
136
ManifestItem.append(self, child)
137
if child.id in self.child_map:
138
print("Warning: Duplicate heading %s" % child.id)
139
self.child_map[child.id] = child
140
141
def _remove_child(self, child):
142
del self.child_map[child.id]
143
ManifestItem._remove_child(self, child)
144
145
def get_test(self, test_id):
146
"""Return a TestNode by test id, or None if no test matches
147
148
:param test_id: The id of the test to look up"""
149
150
return self.child_map.get(test_id)
151
152
def has_test(self, test_id):
153
"""Boolean indicating whether the current test has a known child test
154
with id test id
155
156
:param test_id: The id of the test to look up"""
157
158
return test_id in self.child_map
159
160
@property
161
def url(self):
162
return urljoin(self.url_base,
163
"/".join(self.test_path.split(os.path.sep)))
164
165
def set_lsan(self, run_info, result):
166
"""Set the result of the test in a particular run
167
168
:param run_info: Dictionary of run_info parameters corresponding
169
to this run
170
:param result: Lsan violations detected"""
171
self.update_properties.lsan.set(run_info, result)
172
173
def set_leak_object(self, run_info, result):
174
"""Set the result of the test in a particular run
175
176
:param run_info: Dictionary of run_info parameters corresponding
177
to this run
178
:param result: Leaked objects deletec"""
179
self.update_properties.leak_object.set(run_info, result)
180
181
def set_leak_threshold(self, run_info, result):
182
"""Set the result of the test in a particular run
183
184
:param run_info: Dictionary of run_info parameters corresponding
185
to this run
186
:param result: Total number of bytes leaked"""
187
self.update_properties.leak_threshold.set(run_info, result)
188
189
def update(self, full_update, disable_intermittent):
190
for prop_update in self.update_properties:
191
prop_update.update(full_update,
192
disable_intermittent)
193
194
195
class TestNode(ManifestItem):
196
def __init__(self, node):
197
"""Tree node associated with a particular test in a manifest
198
199
:param node: AST node associated with the test"""
200
201
ManifestItem.__init__(self, node)
202
self.subtests = {}
203
self._from_file = True
204
self.new_disabled = False
205
self.has_result = False
206
self.modified = False
207
self.update_properties = UpdateProperties(
208
self,
209
expected=ExpectedUpdate,
210
max_asserts=MaxAssertsUpdate,
211
min_asserts=MinAssertsUpdate
212
)
213
214
@classmethod
215
def create(cls, test_id):
216
"""Create a TestNode corresponding to a given test
217
218
:param test_type: The type of the test
219
:param test_id: The id of the test"""
220
name = test_id[len(urlsplit(test_id).path.rsplit("/", 1)[0]) + 1:]
221
node = DataNode(name)
222
self = cls(node)
223
224
self._from_file = False
225
return self
226
227
@property
228
def is_empty(self):
229
ignore_keys = {"type"}
230
if set(self._data.keys()) - ignore_keys:
231
return False
232
return all(child.is_empty for child in self.children)
233
234
@property
235
def test_type(self):
236
"""The type of the test represented by this TestNode"""
237
return self.get("type", None)
238
239
@property
240
def id(self):
241
"""The id of the test represented by this TestNode"""
242
return urljoin(self.parent.url, self.name)
243
244
def disabled(self, run_info):
245
"""Boolean indicating whether this test is disabled when run in an
246
environment with the given run_info
247
248
:param run_info: Dictionary of run_info parameters"""
249
250
return self.get("disabled", run_info) is not None
251
252
def set_result(self, run_info, result):
253
"""Set the result of the test in a particular run
254
255
:param run_info: Dictionary of run_info parameters corresponding
256
to this run
257
:param result: Status of the test in this run"""
258
self.update_properties.expected.set(run_info, result)
259
260
def set_asserts(self, run_info, count):
261
"""Set the assert count of a test
262
263
"""
264
self.update_properties.min_asserts.set(run_info, count)
265
self.update_properties.max_asserts.set(run_info, count)
266
267
def append(self, node):
268
child = ManifestItem.append(self, node)
269
self.subtests[child.name] = child
270
271
def get_subtest(self, name):
272
"""Return a SubtestNode corresponding to a particular subtest of
273
the current test, creating a new one if no subtest with that name
274
already exists.
275
276
:param name: Name of the subtest"""
277
278
if name in self.subtests:
279
return self.subtests[name]
280
else:
281
subtest = SubtestNode.create(name)
282
self.append(subtest)
283
return subtest
284
285
def update(self, full_update, disable_intermittent):
286
for prop_update in self.update_properties:
287
prop_update.update(full_update,
288
disable_intermittent)
289
290
291
class SubtestNode(TestNode):
292
def __init__(self, node):
293
assert isinstance(node, DataNode)
294
TestNode.__init__(self, node)
295
296
@classmethod
297
def create(cls, name):
298
node = DataNode(name)
299
self = cls(node)
300
return self
301
302
@property
303
def is_empty(self):
304
if self._data:
305
return False
306
return True
307
308
309
def build_conditional_tree(_, run_info_properties, results):
310
properties, dependent_props = run_info_properties
311
return expectedtree.build_tree(properties, dependent_props, results)
312
313
314
def build_unconditional_tree(_, run_info_properties, results):
315
root = expectedtree.Node(None, None)
316
for run_info, values in iteritems(results):
317
for value, count in iteritems(values):
318
root.result_values[value] += count
319
root.run_info.add(run_info)
320
return root
321
322
323
class PropertyUpdate(object):
324
property_name = None
325
cls_default_value = None
326
value_type = None
327
property_builder = None
328
329
def __init__(self, node):
330
self.node = node
331
self.default_value = self.cls_default_value
332
self.has_result = False
333
self.results = defaultdict(lambda: defaultdict(int))
334
self.update_intermittent = self.node.root.update_intermittent
335
self.remove_intermittent = self.node.root.remove_intermittent
336
337
def run_info_by_condition(self, run_info_index, conditions):
338
run_info_by_condition = defaultdict(list)
339
# A condition might match 0 or more run_info values
340
run_infos = run_info_index.keys()
341
for cond in conditions:
342
for run_info in run_infos:
343
if cond(run_info):
344
run_info_by_condition[cond].append(run_info)
345
346
return run_info_by_condition
347
348
def set(self, run_info, value):
349
self.has_result = True
350
self.node.has_result = True
351
self.check_default(value)
352
value = self.from_result_value(value)
353
self.results[run_info][value] += 1
354
355
def check_default(self, result):
356
return
357
358
def from_result_value(self, value):
359
"""Convert a value from a test result into the internal format"""
360
return value
361
362
def from_ini_value(self, value):
363
"""Convert a value from an ini file into the internal format"""
364
if self.value_type:
365
return self.value_type(value)
366
return value
367
368
def to_ini_value(self, value):
369
"""Convert a value from the internal format to the ini file format"""
370
return str(value)
371
372
def updated_value(self, current, new):
373
"""Given a single current value and a set of observed new values,
374
compute an updated value for the property"""
375
return new
376
377
@property
378
def unconditional_value(self):
379
try:
380
unconditional_value = self.from_ini_value(
381
self.node.get(self.property_name))
382
except KeyError:
383
unconditional_value = self.default_value
384
return unconditional_value
385
386
def update(self,
387
full_update=False,
388
disable_intermittent=None):
389
"""Update the underlying manifest AST for this test based on all the
390
added results.
391
392
This will update existing conditionals if they got the same result in
393
all matching runs in the updated results, will delete existing conditionals
394
that get more than one different result in the updated run, and add new
395
conditionals for anything that doesn't match an existing conditional.
396
397
Conditionals not matched by any added result are not changed.
398
399
When `disable_intermittent` is not None, disable any test that shows multiple
400
unexpected results for the same set of parameters.
401
"""
402
if not self.has_result:
403
return
404
405
property_tree = self.property_builder(self.node.root.run_info_properties,
406
self.results)
407
408
conditions, errors = self.update_conditions(property_tree,
409
full_update)
410
411
for e in errors:
412
if disable_intermittent:
413
condition = e.cond.children[0] if e.cond else None
414
msg = disable_intermittent if isinstance(disable_intermittent, (str, unicode)) else "unstable"
415
self.node.set("disabled", msg, condition)
416
self.node.new_disabled = True
417
else:
418
msg = "Conflicting metadata values for %s" % (
419
self.node.root.test_path)
420
if e.cond:
421
msg += ": %s" % serialize(e.cond).strip()
422
print(msg)
423
424
# If all the values match remove all conditionals
425
# This handles the case where we update a number of existing conditions and they
426
# all end up looking like the post-update default.
427
new_default = self.default_value
428
if conditions and conditions[-1][0] is None:
429
new_default = conditions[-1][1]
430
if all(condition[1] == new_default for condition in conditions):
431
conditions = [(None, new_default)]
432
433
# Don't set the default to the class default
434
if (conditions and
435
conditions[-1][0] is None and
436
conditions[-1][1] == self.default_value):
437
self.node.modified = True
438
conditions = conditions[:-1]
439
440
if self.node.modified:
441
self.node.clear(self.property_name)
442
443
for condition, value in conditions:
444
self.node.set(self.property_name,
445
self.to_ini_value(value),
446
condition)
447
448
def update_conditions(self,
449
property_tree,
450
full_update):
451
# This is complicated because the expected behaviour is complex
452
# The complexity arises from the fact that there are two ways of running
453
# the tool, with a full set of runs (full_update=True) or with partial metadata
454
# (full_update=False). In the case of a full update things are relatively simple:
455
# * All existing conditionals are ignored, with the exception of conditionals that
456
# depend on variables not used by the updater, which are retained as-is
457
# * All created conditionals are independent of each other (i.e. order isn't
458
# important in the created conditionals)
459
# In the case where we don't have a full set of runs, the expected behaviour
460
# is much less clear. This is of course the common case for when a developer
461
# runs the test on their own machine. In this case the assumptions above are untrue
462
# * The existing conditions may be required to handle other platforms
463
# * The order of the conditions may be important, since we don't know if they overlap
464
# e.g. `if os == linux and version == 18.04` overlaps with `if (os != win)`.
465
# So in the case we have a full set of runs, the process is pretty simple:
466
# * Generate the conditionals for the property_tree
467
# * Pick the most common value as the default and add only those conditions
468
# not matching the default
469
# In the case where we have a partial set of runs, things are more complex
470
# and more best-effort
471
# * For each existing conditional, see if it matches any of the run info we
472
# have. In cases where it does match, record the new results
473
# * Where all the new results match, update the right hand side of that
474
# conditional, otherwise remove it
475
# * If this leaves nothing existing, then proceed as with the full update
476
# * Otherwise add conditionals for the run_info that doesn't match any
477
# remaining conditions
478
prev_default = None
479
480
current_conditions = self.node.get_conditions(self.property_name)
481
482
# Ignore the current default value
483
if current_conditions and current_conditions[-1].condition_node is None:
484
self.node.modified = True
485
prev_default = current_conditions[-1].value
486
current_conditions = current_conditions[:-1]
487
488
# If there aren't any current conditions, or there is just a default
489
# value for all run_info, proceed as for a full update
490
if not current_conditions:
491
return self._update_conditions_full(property_tree,
492
prev_default=prev_default)
493
494
conditions = []
495
errors = []
496
497
run_info_index = {run_info: node
498
for node in property_tree
499
for run_info in node.run_info}
500
501
node_by_run_info = {run_info: node
502
for (run_info, node) in iteritems(run_info_index)
503
if node.result_values}
504
505
run_info_by_condition = self.run_info_by_condition(run_info_index,
506
current_conditions)
507
508
run_info_with_condition = set()
509
510
if full_update:
511
# Even for a full update we need to keep hand-written conditions not
512
# using the properties we've specified and not matching any run_info
513
top_level_props, dependent_props = self.node.root.run_info_properties
514
update_properties = set(top_level_props)
515
for item in itervalues(dependent_props):
516
update_properties |= set(dependent_props)
517
for condition in current_conditions:
518
if ((not condition.variables.issubset(update_properties) and
519
not run_info_by_condition[condition])):
520
conditions.append((condition.condition_node,
521
self.from_ini_value(condition.value)))
522
523
new_conditions, errors = self._update_conditions_full(property_tree,
524
prev_default=prev_default)
525
conditions.extend(new_conditions)
526
return conditions, errors
527
528
# Retain existing conditions if they match the updated values
529
for condition in current_conditions:
530
# All run_info that isn't handled by some previous condition
531
all_run_infos_condition = run_info_by_condition[condition]
532
run_infos = {item for item in all_run_infos_condition
533
if item not in run_info_with_condition}
534
535
if not run_infos:
536
# Retain existing conditions that don't match anything in the update
537
conditions.append((condition.condition_node,
538
self.from_ini_value(condition.value)))
539
continue
540
541
# Set of nodes in the updated tree that match the same run_info values as the
542
# current existing node
543
nodes = [node_by_run_info[run_info] for run_info in run_infos
544
if run_info in node_by_run_info]
545
# If all the values are the same, update the value
546
if nodes and all(set(node.result_values.keys()) == set(nodes[0].result_values.keys()) for node in nodes):
547
current_value = self.from_ini_value(condition.value)
548
try:
549
new_value = self.updated_value(current_value,
550
nodes[0].result_values)
551
except ConditionError as e:
552
errors.append(e)
553
continue
554
if new_value != current_value:
555
self.node.modified = True
556
conditions.append((condition.condition_node, new_value))
557
run_info_with_condition |= set(run_infos)
558
else:
559
# Don't append this condition
560
self.node.modified = True
561
562
new_conditions, new_errors = self.build_tree_conditions(property_tree,
563
run_info_with_condition,
564
prev_default)
565
if new_conditions:
566
self.node.modified = True
567
568
conditions.extend(new_conditions)
569
errors.extend(new_errors)
570
571
return conditions, errors
572
573
def _update_conditions_full(self,
574
property_tree,
575
prev_default=None):
576
self.node.modified = True
577
conditions, errors = self.build_tree_conditions(property_tree,
578
set(),
579
prev_default)
580
581
return conditions, errors
582
583
def build_tree_conditions(self,
584
property_tree,
585
run_info_with_condition,
586
prev_default=None):
587
conditions = []
588
errors = []
589
590
value_count = defaultdict(int)
591
592
def to_count_value(v):
593
if v is None:
594
return v
595
# Need to count the values in a hashable type
596
count_value = self.to_ini_value(v)
597
if isinstance(count_value, list):
598
count_value = tuple(count_value)
599
return count_value
600
601
602
queue = deque([(property_tree, [])])
603
while queue:
604
node, parents = queue.popleft()
605
parents_and_self = parents + [node]
606
if node.result_values and any(run_info not in run_info_with_condition
607
for run_info in node.run_info):
608
prop_set = [(item.prop, item.value) for item in parents_and_self if item.prop]
609
value = node.result_values
610
error = None
611
if parents:
612
try:
613
value = self.updated_value(None, value)
614
except ConditionError:
615
expr = make_expr(prop_set, value)
616
error = ConditionError(expr)
617
expr = make_expr(prop_set, value)
618
else:
619
# The root node needs special handling
620
expr = None
621
try:
622
value = self.updated_value(self.unconditional_value,
623
value)
624
except ConditionError:
625
error = ConditionError(expr)
626
# If we got an error for the root node, re-add the previous
627
# default value
628
if prev_default:
629
conditions.append((None, prev_default))
630
if error is None:
631
count_value = to_count_value(value)
632
value_count[count_value] += len(node.run_info)
633
634
if error is None:
635
conditions.append((expr, value))
636
else:
637
errors.append(error)
638
639
for child in node.children:
640
queue.append((child, parents_and_self))
641
642
conditions = conditions[::-1]
643
644
# If we haven't set a default condition, add one and remove all the conditions
645
# with the same value
646
if value_count and (not conditions or conditions[-1][0] is not None):
647
# Sort in order of occurence, prioritising values that match the class default
648
# or the previous default
649
cls_default = to_count_value(self.default_value)
650
prev_default = to_count_value(prev_default)
651
commonest_value = max(value_count, key=lambda x:(value_count.get(x),
652
x == cls_default,
653
x == prev_default))
654
if isinstance(commonest_value, tuple):
655
commonest_value = list(commonest_value)
656
commonest_value = self.from_ini_value(commonest_value)
657
conditions = [item for item in conditions if item[1] != commonest_value]
658
conditions.append((None, commonest_value))
659
660
return conditions, errors
661
662
663
class ExpectedUpdate(PropertyUpdate):
664
property_name = "expected"
665
property_builder = build_conditional_tree
666
667
def check_default(self, result):
668
if self.default_value is not None:
669
assert self.default_value == result.default_expected
670
else:
671
self.default_value = result.default_expected
672
673
def from_result_value(self, result):
674
# When we are updating intermittents, we need to keep a record of any existing
675
# intermittents to pass on when building the property tree and matching statuses and
676
# intermittents to the correct run info - this is so we can add them back into the
677
# metadata aligned with the right conditions, unless specified not to with
678
# self.remove_intermittent.
679
# The (status, known_intermittent) tuple is counted when the property tree is built, but
680
# the count value only applies to the first item in the tuple, the status from that run,
681
# when passed to `updated_value`.
682
if (not self.update_intermittent or
683
self.remove_intermittent or
684
not result.known_intermittent):
685
return result.status
686
return result.status + result.known_intermittent
687
688
def to_ini_value(self, value):
689
if isinstance(value, (list, tuple)):
690
return [str(item) for item in value]
691
return str(value)
692
693
def updated_value(self, current, new):
694
if len(new) > 1 and not self.update_intermittent and not isinstance(current, list):
695
raise ConditionError
696
697
counts = {}
698
for status, count in iteritems(new):
699
if isinstance(status, tuple):
700
counts[status[0]] = count
701
counts.update({intermittent: 0 for intermittent in status[1:] if intermittent not in counts})
702
else:
703
counts[status] = count
704
705
if not (self.update_intermittent or isinstance(current, list)):
706
return list(counts)[0]
707
708
# Reorder statuses first based on counts, then based on status priority if there are ties.
709
# Counts with 0 are considered intermittent.
710
statuses = ["OK", "PASS", "FAIL", "ERROR", "TIMEOUT", "CRASH"]
711
status_priority = {value: i for i, value in enumerate(statuses)}
712
sorted_new = sorted(iteritems(counts), key=lambda x:(-1 * x[1],
713
status_priority.get(x[0],
714
len(status_priority))))
715
expected = []
716
for status, count in sorted_new:
717
# If we are not removing existing recorded intermittents, with a count of 0,
718
# add them in to expected.
719
if count > 0 or not self.remove_intermittent:
720
expected.append(status)
721
if self.update_intermittent:
722
if len(expected) == 1:
723
return expected[0]
724
return expected
725
726
# If nothing has changed and not self.update_intermittent, preserve existing
727
# intermittent.
728
if set(expected).issubset(set(current)):
729
return current
730
# If we are not updating intermittents, return the status with the highest occurence.
731
return expected[0]
732
733
734
class MaxAssertsUpdate(PropertyUpdate):
735
"""For asserts we always update the default value and never add new conditionals.
736
The value we set as the default is the maximum the current default or one more than the
737
number of asserts we saw in any configuration."""
738
739
property_name = "max-asserts"
740
cls_default_value = 0
741
value_type = int
742
property_builder = build_unconditional_tree
743
744
def updated_value(self, current, new):
745
if any(item > current for item in new):
746
return max(new) + 1
747
return current
748
749
750
class MinAssertsUpdate(PropertyUpdate):
751
property_name = "min-asserts"
752
cls_default_value = 0
753
value_type = int
754
property_builder = build_unconditional_tree
755
756
def updated_value(self, current, new):
757
if any(item < current for item in new):
758
rv = min(new) - 1
759
else:
760
rv = current
761
return max(rv, 0)
762
763
764
class AppendOnlyListUpdate(PropertyUpdate):
765
cls_default_value = []
766
property_builder = build_unconditional_tree
767
768
def updated_value(self, current, new):
769
if current is None:
770
rv = set()
771
else:
772
rv = set(current)
773
774
for item in new:
775
if item is None:
776
continue
777
elif isinstance(item, (str, unicode)):
778
rv.add(item)
779
else:
780
rv |= item
781
782
return sorted(rv)
783
784
785
class LsanUpdate(AppendOnlyListUpdate):
786
property_name = "lsan-allowed"
787
property_builder = build_unconditional_tree
788
789
def from_result_value(self, result):
790
# If we have an allowed_match that matched, return None
791
# This value is ignored later (because it matches the default)
792
# We do that because then if we allow a failure in foo/__dir__.ini
793
# we don't want to update foo/bar/__dir__.ini with the same rule
794
if result[1]:
795
return None
796
# Otherwise return the topmost stack frame
797
# TODO: there is probably some improvement to be made by looking for a "better" stack frame
798
return result[0][0]
799
800
def to_ini_value(self, value):
801
return value
802
803
804
class LeakObjectUpdate(AppendOnlyListUpdate):
805
property_name = "leak-allowed"
806
property_builder = build_unconditional_tree
807
808
def from_result_value(self, result):
809
# If we have an allowed_match that matched, return None
810
if result[1]:
811
return None
812
# Otherwise return the process/object name
813
return result[0]
814
815
816
class LeakThresholdUpdate(PropertyUpdate):
817
property_name = "leak-threshold"
818
cls_default_value = {}
819
property_builder = build_unconditional_tree
820
821
def from_result_value(self, result):
822
return result
823
824
def to_ini_value(self, data):
825
return ["%s:%s" % item for item in sorted(iteritems(data))]
826
827
def from_ini_value(self, data):
828
rv = {}
829
for item in data:
830
key, value = item.split(":", 1)
831
rv[key] = int(float(value))
832
return rv
833
834
def updated_value(self, current, new):
835
if current:
836
rv = current.copy()
837
else:
838
rv = {}
839
for process, leaked_bytes, threshold in new:
840
# If the value is less than the threshold but there isn't
841
# an old value we must have inherited the threshold from
842
# a parent ini file so don't any anything to this one
843
if process not in rv and leaked_bytes < threshold:
844
continue
845
if leaked_bytes > rv.get(process, 0):
846
# Round up to nearest 50 kb
847
boundary = 50 * 1024
848
rv[process] = int(boundary * ceil(float(leaked_bytes) / boundary))
849
return rv
850
851
852
def make_expr(prop_set, rhs):
853
"""Create an AST that returns the value ``status`` given all the
854
properties in prop_set match.
855
856
:param prop_set: tuple of (property name, value) pairs for each
857
property in this expression and the value it must match
858
:param status: Status on RHS when all the given properties match
859
"""
860
root = ConditionalNode()
861
862
assert len(prop_set) > 0
863
864
expressions = []
865
for prop, value in prop_set:
866
if value not in (True, False):
867
expressions.append(
868
BinaryExpressionNode(
869
BinaryOperatorNode("=="),
870
VariableNode(prop),
871
make_node(value)))
872
else:
873
if value:
874
expressions.append(VariableNode(prop))
875
else:
876
expressions.append(
877
UnaryExpressionNode(
878
UnaryOperatorNode("not"),
879
VariableNode(prop)
880
))
881
if len(expressions) > 1:
882
prev = expressions[-1]
883
for curr in reversed(expressions[:-1]):
884
node = BinaryExpressionNode(
885
BinaryOperatorNode("and"),
886
curr,
887
prev)
888
prev = node
889
else:
890
node = expressions[0]
891
892
root.append(node)
893
rhs_node = make_value_node(rhs)
894
root.append(rhs_node)
895
896
return root
897
898
899
def make_node(value):
900
if type(value) in (int, float, long):
901
node = NumberNode(value)
902
elif type(value) in (str, unicode):
903
node = StringNode(unicode(value))
904
elif hasattr(value, "__iter__"):
905
node = ListNode()
906
for item in value:
907
node.append(make_node(item))
908
return node
909
910
911
def make_value_node(value):
912
if type(value) in (int, float, long):
913
node = ValueNode(value)
914
elif type(value) in (str, unicode):
915
node = ValueNode(unicode(value))
916
elif hasattr(value, "__iter__"):
917
node = ListNode()
918
for item in value:
919
node.append(make_node(item))
920
else:
921
raise ValueError("Don't know how to convert %s into node" % type(value))
922
return node
923
924
925
def get_manifest(metadata_root, test_path, url_base, run_info_properties, update_intermittent, remove_intermittent):
926
"""Get the ExpectedManifest for a particular test path, or None if there is no
927
metadata stored for that test path.
928
929
:param metadata_root: Absolute path to the root of the metadata directory
930
:param test_path: Path to the test(s) relative to the test root
931
:param url_base: Base url for serving the tests in this manifest"""
932
manifest_path = expected.expected_path(metadata_root, test_path)
933
try:
934
with open(manifest_path) as f:
935
rv = compile(f, test_path, url_base,
936
run_info_properties, update_intermittent, remove_intermittent)
937
except IOError:
938
return None
939
return rv
940
941
942
def compile(manifest_file, test_path, url_base, run_info_properties, update_intermittent, remove_intermittent):
943
return conditional.compile(manifest_file,
944
data_cls_getter=data_cls_getter,
945
test_path=test_path,
946
url_base=url_base,
947
run_info_properties=run_info_properties,
948
update_intermittent=update_intermittent,
949
remove_intermittent=remove_intermittent)