2 # Copyright 2013 The Swarming Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0 that
4 # can be found in the LICENSE file.
11 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 sys.path.insert(0, ROOT_DIR)
14 from utils import short_expression_finder
15 from utils.short_expression_finder import ShortExpressionFinder, partitions
18 # Access to a protected member XXX of a client class
19 # pylint: disable=W0212
22 def matching_configs(expr, variables_and_values):
23 """Return configs for which expr evaluates to true."""
24 variables, values = zip(*variables_and_values)
26 config for config in itertools.product(*values)
27 if eval(expr, dict(zip(variables, config)))
31 class Tests(unittest.TestCase):
32 def test_simple(self):
33 sef = ShortExpressionFinder([('var', [17])])
34 self.assertEqual('var==17', sef.get_expr([(17,)]))
36 def test_two_variables_four_values(self):
37 vals1, vals2 = (1, 2), ("ab", "cd")
38 sef = ShortExpressionFinder([('var1', vals1), ('var2', vals2)])
39 all_configs = tuple(itertools.product(vals1, vals2))
42 self.assertEqual('var1==%d and var2=="%s"' % (val1, val2),
43 sef.get_expr([(val1, val2)]))
45 self.assertEqual('var1==%d and (var2=="ab" or var2=="cd")' % val1,
46 sef.get_expr([(val1, "ab"), (val1, "cd")]))
48 self.assertEqual('(var1==1 or var1==2) and var2=="%s"' % val2,
49 sef.get_expr([(1, val2), (2, val2)]))
50 self.assertEqual('(var1==1 or var1==2) and (var2=="ab" or var2=="cd")',
51 sef.get_expr(all_configs))
53 def check_expr(self, expr, variables_and_values):
54 """Check that ShortExpressionFinder returns an expression that's logically
55 equivalent to |expr| and equally simple, when given the matching configs.
57 configs = matching_configs(expr, variables_and_values)
58 output_expr = ShortExpressionFinder(variables_and_values).get_expr(configs)
59 output_configs = matching_configs(output_expr, variables_and_values)
60 self.assertEqual(configs, output_configs)
61 self.assertEqual(expr.count('=='), output_expr.count('=='))
62 self.assertEqual(expr.count('('), output_expr.count('('))
64 def test_single_nontrivial_region(self):
65 self.check_expr('((OS=="linux" or OS=="mac" or OS=="win") and chromeos==0)'
66 ' or (OS=="linux" and chromeos==1)',
67 [('OS', 'linux mac win'.split()), ('chromeos', (0, 1))])
69 self.check_expr('(OS=="linux" or OS=="mac" or OS=="win") and chromeos==0',
70 [('OS', 'linux mac win'.split()), ('chromeos', (0, 1))])
72 self.check_expr('OS=="linux" and (chromeos==0 or chromeos==1)',
73 [('OS', 'linux mac win'.split()), ('chromeos', (0, 1))])
75 def test_multiple_nontrivial_regions(self):
76 # two disjoint regions of 1*2*1=2 and 2*1*2=4 configs, no overlap
78 '(p==2 and (q=="a" or q=="b") and r==10)'
79 ' or ((p==1 or p==2) and q=="c" and (r==8 or r==10))',
80 [('p', (1, 2, 3)), ('q', ('a', 'b', 'c')), ('r', (8, 9, 10))])
82 # two regions of 4 and 8 configs with overlap
84 '((p==1 or p==2) and (q=="a" or q=="b") and r==9)'
85 ' or ((p==2 or p==3) and q=="a" and (r==8 or r==9))',
86 [('p', (1, 2, 3)), ('q', ('a', 'b', 'c')), ('r', (8, 9, 10))])
88 # All configs except (p, q, r) == (2, 4, 6). There are simpler expressions
89 # for this that don't fit ShortExpressionFinder's grammar.
90 self.check_expr('((p==1 or p==2) and (q==3 or q==4) and r==5)'
91 ' or ((p==1 or p==2) and q==3 and r==6)'
92 ' or (p==1 and q==4 and r==6)',
93 [('p', (1, 2)), ('q', (3, 4)), ('r', (5, 6))])
95 def test_100_variables(self):
96 # This is kind of a cheat because the complexity is ((2^k)-1)^n for n
97 # variables of k values each. With k=2 this would be hopelessly slow.
98 self.check_expr(' and '.join('var%d=="val%d"' % (n, n) for n in range(100)),
99 [('var%d' % n, ('val%d' % n,)) for n in range(100)])
101 def test_short_expression_finder_failure(self):
102 # End users should never see these failures so it doesn't matter much how
104 self.assertRaises(AssertionError, ShortExpressionFinder, [])
105 self.assertRaises(AssertionError,
106 ShortExpressionFinder, [('x', (1, 2)), ('no_values', ())])
107 self.assertRaises(TypeError,
108 ShortExpressionFinder, [('bad_type', (1, 2.5))])
109 self.assertRaises(AssertionError,
110 ShortExpressionFinder, [('bad name', (1, 2))])
111 self.assertRaises(AssertionError,
112 ShortExpressionFinder, [('x', ('bad value',))])
113 self.assertRaises(TypeError,
114 ShortExpressionFinder, [('x', (1, 'mixed_values'))])
115 self.assertRaises(AssertionError,
116 ShortExpressionFinder([('no_configs', (1, 2))]).get_expr, [])
117 self.assertRaises(AssertionError,
118 ShortExpressionFinder([('no_such_value', (1, 2))]).get_expr, [(3,)])
119 self.assertRaises(AssertionError,
120 ShortExpressionFinder([('wrong_length', (1, 2))]).get_expr, [(1, 3)])
122 def test_partitions(self):
123 self.assertEqual(((None, None),), tuple(partitions(0, 2)))
124 self.assertEqual((), tuple(partitions(1, 2)))
126 def count_partitions(parts, remaining):
128 if parts == ((None, None),):
129 self.assertEqual(0, remaining)
131 return sum(count_partitions(ns, remaining - n) for n, ns in parts)
133 # http://oeis.org/A002865
134 expected = [1, 0, 1, 1, 2, 2, 4, 4, 7, 8, 12, 14, 21, 24, 34, 41, 55, 66]
135 for n, count in enumerate(expected):
136 actual_count = count_partitions(partitions(n, 2), n)
137 self.assertEqual(count, actual_count)
139 # This also checks that the call doesn't compute all
140 # 23,127,843,459,154,899,464,880,444,632,250 partitions up front.
141 self.assertEqual(501, len(tuple(partitions(1000, 1))))
143 def test_nones(self):
144 ShortExpressionFinder([('foo', ('a',))])
145 with self.assertRaises(TypeError):
146 ShortExpressionFinder([('foo', ('a', None))])
148 def test_complex(self):
149 # An end-to-end test that looks at behavior with 4 degrees (variables).
151 # For example component build vs static build.
154 ('OS', ('l', 'm', 'w')),
155 ('brand', ('C', 'GC', 'Y')),
159 [('s', 0, 'l', 'C')],
160 'lib=="s" and cros==0 and OS=="l" and brand=="C"',
163 [('s', 0, 'l', 'C'), ('s', 0, 'm', 'C')],
164 'lib=="s" and cros==0 and (OS=="l" or OS=="m") and brand=="C"',
167 [('s', 0, 'l', 'C'), ('s', 0, 'm', 'C'), ('s', 0, 'm', 'GC')],
168 '(lib=="s" and cros==0 and OS=="l" and brand=="C") or '
169 '(lib=="s" and cros==0 and OS=="m" and (brand=="C" or brand=="GC"))'
172 [('s', 0, 'l', 'C'), ('s', 0, 'm', 'C'), ('s', 0, 'w', 'C')],
173 'lib=="s" and cros==0 and (OS=="l" or OS=="m" or OS=="w") and '
178 ('s', 1, 'l', 'C'), ('s', 0, 'l', 'C'), ('s', 0, 'm', 'C'),
181 # TODO(maruel): Could have been written slightly more efficiently.
182 # 'lib=="s" and 'brand=="C" and (...)'
183 '(lib=="s" and cros==1 and OS=="l" and brand=="C") or '
184 '(lib=="s" and cros==0 and (OS=="l" or OS=="m" or OS=="w") and '
188 [('s', None, None, None)],
192 [('s', 1, None, None)],
193 'lib=="s" and cros==1',
196 [(None, 1, None, None)],
200 [(None, 1, None, None), (None, 0, 'w', None), (None, 0, 'm', None)],
201 # Note the ordering of the specification is lost.
202 '(cros==0 and (OS=="m" or OS=="w")) or cros==1',
205 [('s', 1, 'l', None), ('c', 0, 'w', None), ('s', 0, 'm', None)],
206 # TODO(maruel): Could have been written slightly more efficiently.
207 '(lib=="s" and cros==0 and OS=="m") or '
208 '(lib=="c" and cros==0 and OS=="w") or '
209 '(lib=="s" and cros==1 and OS=="l")',
212 [('s', 1, 'l', None), ('c', 0, 'w', None), ('c', 0, 'm', None)],
213 '(lib=="s" and cros==1 and OS=="l") or '
214 '(lib=="c" and cros==0 and (OS=="m" or OS=="w"))',
218 (None, 1, 'l', 'GC'), (None, 0, 'l', 'GC'), (None, None, 'm', 'GC'),
219 (None, None, 'w', 'GC'),
221 '((cros==0 or cros==1) and OS=="l" and brand=="GC") or '
222 '((OS=="m" or OS=="w") and brand=="GC")',
225 for i, (data, expected) in enumerate(expectations):
226 s = short_expression_finder.ShortExpressionFinder(variables)
227 actual = s.get_expr(data)
229 expected, actual, '\n%r\n%r\n%r\n%s' % (data, expected, actual, i))
232 class TestPrivateCode(unittest.TestCase):
233 # Tests the non-exported functions.
234 def test_calculate_cost_simple(self):
237 {2: {1: (1,)}}, short_expression_finder._calculate_costs(values, 1, 1))
239 def test_calculate_cost_1_x_0(self):
242 self.assertEqual({}, short_expression_finder._calculate_costs(values, 1, 1))
244 def test_calculate_cost_1_x_2(self):
245 values = ([1, 2], [3])
256 expected, short_expression_finder._calculate_costs(values, 1, 1))
258 def test_calculate_cost_2_x_2(self):
259 values = ([1, 2], [3, 4])
278 expected, short_expression_finder._calculate_costs(values, 1, 1))
280 def test_format_or(self):
281 values = ('linux', 'mac', 'win')
283 # No variable is bound. It's the 'global variable'.
285 (1, ('OS=="linux"', 1)),
286 (2, ('OS=="mac"', 1)),
287 (3, ('OS=="linux" or OS=="mac"', 2)),
288 (4, ('OS=="win"', 1)),
289 (5, ('OS=="linux" or OS=="win"', 2)),
290 (6, ('OS=="mac" or OS=="win"', 2)),
291 (7, ('OS=="linux" or OS=="mac" or OS=="win"', 3)),
293 for data, expected in expectations:
295 expected, short_expression_finder._format_or('OS', values, data))
297 def test_format_and(self):
298 # Create a 2D matrix.
299 variables = ('chromeos', 'OS')
300 values = ((0, 1), ('linux', 'mac', 'win'))
302 # No variable is bound. It's the 'global variable'.
304 ((0, 1), ('OS=="linux"', 1)),
305 ((0, 2), ('OS=="mac"', 1)),
306 ((0, 3), ('OS=="linux" or OS=="mac"', 1)),
307 ((1, 4), ('chromeos==0 and OS=="win"', 2)),
308 ((1, 5), ('chromeos==0 and (OS=="linux" or OS=="win")', 2)),
309 # _format_and() just renders what it receives.
312 ('(chromeos==0 or chromeos==1) and (OS=="linux" or OS=="win")', 2),
315 for data, expected in expectations:
318 short_expression_finder._format_and(variables, values, data))
320 def test_format_expr(self):
321 # Create a 2D matrix.
322 variables = ('chromeos', 'OS')
323 values = ((0, 1), ('linux', 'mac', 'win'))
326 # Unbounded variable has a 0 as its bitfield.
331 # Unbounded variable has a 0 as its bitfield.
336 # The function expects already reduced and sorted equations. It won't
337 # reduce it on your behalf.
339 ('OS=="linux" or (chromeos==1 and OS=="linux")', 2),
343 ('chromeos==1 or OS=="win"', 2),
346 # Notice smart use of parenthesis.
348 ('(chromeos==0 and OS=="linux") or OS=="linux" or OS=="win"', 2),
351 for data, expected in expectations:
354 short_expression_finder._format_expr(variables, values, data))
356 def test_internal_guts_2_variables(self):
357 # chromeos can be: 0 or 1.
358 # OS can be: linux, mac or win.
359 variables = ('chromeos', 'OS')
360 values = ((0, 1), ('linux', 'mac', 'win'))
362 # 1. is input data to _get_expr. It is all the possibilities that should be
364 # 2. is (bits, values) output from _get_expr, that is input in
365 # _find_reduced_cost.
366 # 3. is (assertions) output from _find_reduced_cost, that is input in
368 # 4. is string output from _format_expr.
374 ('chromeos==0 and OS=="linux"', 2),
377 [(1, 'linux'), (0, 'mac')],
380 ('(chromeos==1 and OS=="linux") or (chromeos==0 and OS=="mac")', 2),
383 [(1, 'linux'), (0, 'mac'), (0, 'win')],
387 '(chromeos==1 and OS=="linux") or '
388 '(chromeos==0 and (OS=="mac" or OS=="win"))',
393 for line in expectations:
394 # Note that none of these functions support free variable. It's
395 # implemented by ShortExpressionFinder.get_expr(.
396 to_get_expr, bits, to_format_expr, final_expected = line
399 short_expression_finder._get_expr(values, to_get_expr))
402 short_expression_finder._find_reduced_cost(1, 1, values, bits))
405 short_expression_finder._format_expr(
406 variables, values, to_format_expr))
408 def test_internal_guts_4_variables(self):
409 # Create a 4D matrix.
410 # See test_internal_guts_2_variables for the explanations.
411 variables = ('lib', 'cros', 'OS', 'brand')
412 values = (('s', 'c'), (0, 1), ('l', 'm', 'w'), ('C', 'GC', 'Y'))
415 [('s', 0, 'l', 'C')],
418 ('lib=="s" and cros==0 and OS=="l" and brand=="C"', 4),
421 # 2nd value for 1th variable.
422 [('c', 0, 'l', 'C')],
425 ('lib=="c" and cros==0 and OS=="l" and brand=="C"', 4),
428 # 2nd value for 2th variable.
429 [('s', 1, 'l', 'C')],
432 ('lib=="s" and cros==1 and OS=="l" and brand=="C"', 4),
435 # 2nd value for 3th variable.
436 [('s', 0, 'm', 'C')],
439 ('lib=="s" and cros==0 and OS=="m" and brand=="C"', 4),
442 # 3nd value for 3th variable.
443 [('s', 0, 'w', 'C')],
445 ((1, 1, 4, 1),), # bitfields, not numbers.
446 ('lib=="s" and cros==0 and OS=="w" and brand=="C"', 4),
449 # 2nd value for 4th variable.
450 [('s', 0, 'l', 'GC')],
453 ('lib=="s" and cros==0 and OS=="l" and brand=="GC"', 4),
456 # Last bit that can be set, all values are the last.
457 # 100000000000000000000000000000000000 is 36th bit == 2*2*3*3.
458 [('c', 1, 'w', 'Y')],
461 ('lib=="c" and cros==1 and OS=="w" and brand=="Y"', 4),
464 # Same condition twice doesn't affect the result.
465 [('s', 0, 'l', 'C'), ('s', 0, 'l', 'C')],
466 # One real condition, only one bit set.
469 ('lib=="s" and cros==0 and OS=="l" and brand=="C"', 4),
472 # All values for 1st variable.
473 [('s', 0, 'l', 'C'), ('c', 0, 'l', 'C')],
477 ('(lib=="s" or lib=="c") and cros==0 and OS=="l" and brand=="C"', 4),
480 # All values for 2nd variable.
481 [('s', 0, 'l', 'C'), ('s', 1, 'l', 'C')],
485 ('lib=="s" and (cros==0 or cros==1) and OS=="l" and brand=="C"', 4),
488 for line in expectations:
489 # Note that none of these functions support free variable. It's
490 # implemented by ShortExpressionFinder.get_expr().
491 to_get_expr, bits, to_format_expr, final_expected = line
494 short_expression_finder._get_expr(values, to_get_expr))
497 short_expression_finder._find_reduced_cost(1, 1, values, bits))
500 short_expression_finder._format_expr(
501 variables, values, to_format_expr))
504 if __name__ == '__main__':
505 VERBOSE = '-v' in sys.argv