blob: 6e6ce997a2b7a417e7143f084a2d4ade375128d5 [file] [log] [blame]
J. Richard Barnette96db3492015-03-27 17:23:52 -07001#!/usr/bin/env python
2# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Prathmesh Prabhu58aede82017-11-09 13:34:25 -08006import collections
J. Richard Barnettef6839282015-06-01 16:00:35 -07007import itertools
J. Richard Barnettef60a1ee2015-06-02 10:52:37 -07008import logging
J. Richard Barnette96db3492015-03-27 17:23:52 -07009import os
10import unittest
11
12import common
Aviv Keshet7ee95862016-08-30 15:18:27 -070013from autotest_lib.server.lib import status_history
J. Richard Barnette96db3492015-03-27 17:23:52 -070014from autotest_lib.site_utils import lab_inventory
J. Richard Barnette96db3492015-03-27 17:23:52 -070015
16
xixuan12ce04f2016-03-10 13:16:30 -080017class _FakeHost(object):
18 """Class to mock `Host` in _FakeHostHistory for testing."""
19
Richard Barnette5de01eb2017-12-15 09:53:42 -080020 def __init__(self, hostname):
xixuan12ce04f2016-03-10 13:16:30 -080021 self.hostname = hostname
22
23
J. Richard Barnette96db3492015-03-27 17:23:52 -070024class _FakeHostHistory(object):
25 """Class to mock `HostJobHistory` for testing."""
26
Richard Barnette5de01eb2017-12-15 09:53:42 -080027 def __init__(self, model, pool, status, hostname=''):
28 self.host_model = model
29 self.host_board = model + '_board'
Prathmesh Prabhu58aede82017-11-09 13:34:25 -080030 self.host_pool = pool
31 self.status = status
Richard Barnette5de01eb2017-12-15 09:53:42 -080032 self.host = _FakeHost(hostname)
J. Richard Barnette96db3492015-03-27 17:23:52 -070033
J. Richard Barnette96db3492015-03-27 17:23:52 -070034 def last_diagnosis(self):
35 """Return the recorded diagnosis."""
Prathmesh Prabhu58aede82017-11-09 13:34:25 -080036 return self.status, None
J. Richard Barnette96db3492015-03-27 17:23:52 -070037
38
J. Richard Barnettef6839282015-06-01 16:00:35 -070039class _FakeHostLocation(object):
40 """Class to mock `HostJobHistory` for location sorting."""
41
42 _HOSTNAME_FORMAT = 'chromeos%d-row%d-rack%d-host%d'
43
J. Richard Barnettef6839282015-06-01 16:00:35 -070044 def __init__(self, location):
45 self.hostname = self._HOSTNAME_FORMAT % location
46
J. Richard Barnettef6839282015-06-01 16:00:35 -070047 @property
48 def host(self):
49 """Return a fake host object with a hostname."""
50 return self
51
52
J. Richard Barnette96db3492015-03-27 17:23:52 -070053# Status values that may be returned by `HostJobHistory`.
54#
Richard Barnette5de01eb2017-12-15 09:53:42 -080055# These merely rename the corresponding values in `status_history`
56# for convenience.
J. Richard Barnette96db3492015-03-27 17:23:52 -070057
58_WORKING = status_history.WORKING
Richard Barnette5de01eb2017-12-15 09:53:42 -080059_UNUSED = status_history.UNUSED
60_BROKEN = status_history.BROKEN
61_UNKNOWN = status_history.UNKNOWN
J. Richard Barnette96db3492015-03-27 17:23:52 -070062
63
Richard Barnette5de01eb2017-12-15 09:53:42 -080064class HostSetInventoryTestCase(unittest.TestCase):
65 """Unit tests for class `_HostSetInventory`.
J. Richard Barnette96db3492015-03-27 17:23:52 -070066
67 Coverage is quite basic: mostly just enough to make sure every
68 function gets called, and to make sure that the counting knows
69 the difference between 0 and 1.
70
Richard Barnette5de01eb2017-12-15 09:53:42 -080071 The testing also ensures that all known status values that can be
72 returned by `HostJobHistory` are counted as expected.
J. Richard Barnette96db3492015-03-27 17:23:52 -070073 """
74
75 def setUp(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -080076 super(HostSetInventoryTestCase, self).setUp()
77 self.histories = lab_inventory._HostSetInventory()
J. Richard Barnette96db3492015-03-27 17:23:52 -070078
J. Richard Barnette96db3492015-03-27 17:23:52 -070079 def _add_host(self, status):
Richard Barnette5de01eb2017-12-15 09:53:42 -080080 fake = _FakeHostHistory('zebra', lab_inventory.SPARE_POOL, status)
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -080081 self.histories.record_host(fake)
J. Richard Barnette96db3492015-03-27 17:23:52 -070082
xixuan12ce04f2016-03-10 13:16:30 -080083 def _check_counts(self, working, broken, idle):
J. Richard Barnette96db3492015-03-27 17:23:52 -070084 """Check that pool counts match expectations.
85
Richard Barnette5de01eb2017-12-15 09:53:42 -080086 Asserts that `get_working()`, `get_broken()`, and `get_idle()`
87 return the given expected values. Also assert that
88 `get_total()` is the sum of all counts.
J. Richard Barnette96db3492015-03-27 17:23:52 -070089
90 @param working The expected total of working devices.
91 @param broken The expected total of broken devices.
Richard Barnette5de01eb2017-12-15 09:53:42 -080092 @param idle The expected total of idle devices.
J. Richard Barnette96db3492015-03-27 17:23:52 -070093 """
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -080094 self.assertEqual(self.histories.get_working(), working)
95 self.assertEqual(self.histories.get_broken(), broken)
96 self.assertEqual(self.histories.get_idle(), idle)
97 self.assertEqual(self.histories.get_total(),
xixuan12ce04f2016-03-10 13:16:30 -080098 working + broken + idle)
J. Richard Barnette96db3492015-03-27 17:23:52 -070099
J. Richard Barnette96db3492015-03-27 17:23:52 -0700100 def test_empty(self):
101 """Test counts when there are no DUTs recorded."""
xixuan12ce04f2016-03-10 13:16:30 -0800102 self._check_counts(0, 0, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700103
xixuan12ce04f2016-03-10 13:16:30 -0800104 def test_broken(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800105 """Test counting for broken DUTs."""
xixuan12ce04f2016-03-10 13:16:30 -0800106 self._add_host(_BROKEN)
107 self._check_counts(0, 1, 0)
108
Richard Barnette5de01eb2017-12-15 09:53:42 -0800109 def test_working(self):
110 """Test counting for working DUTs."""
111 self._add_host(_WORKING)
112 self._check_counts(1, 0, 0)
113
xixuan12ce04f2016-03-10 13:16:30 -0800114 def test_idle(self):
115 """Testing counting for idle status values."""
116 self._add_host(_UNUSED)
117 self._check_counts(0, 0, 1)
118 self._add_host(_UNKNOWN)
119 self._check_counts(0, 0, 2)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700120
J. Richard Barnette96db3492015-03-27 17:23:52 -0700121 def test_working_then_broken(self):
122 """Test counts after adding a working and then a broken DUT."""
123 self._add_host(_WORKING)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700124 self._add_host(_BROKEN)
xixuan12ce04f2016-03-10 13:16:30 -0800125 self._check_counts(1, 1, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700126
J. Richard Barnette96db3492015-03-27 17:23:52 -0700127 def test_broken_then_working(self):
128 """Test counts after adding a broken and then a working DUT."""
129 self._add_host(_BROKEN)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700130 self._add_host(_WORKING)
xixuan12ce04f2016-03-10 13:16:30 -0800131 self._check_counts(1, 1, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700132
133
Richard Barnette5de01eb2017-12-15 09:53:42 -0800134class PoolSetInventoryTestCase(unittest.TestCase):
135 """Unit tests for class `_PoolSetInventory`.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700136
Richard Barnette5de01eb2017-12-15 09:53:42 -0800137 Coverage is quite basic: just enough to make sure every function
138 gets called, and to make sure that the counting knows the difference
139 between 0 and 1.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700140
Richard Barnette5de01eb2017-12-15 09:53:42 -0800141 The tests make sure that both individual pool counts and totals are
142 counted correctly.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700143 """
144
Richard Barnette5de01eb2017-12-15 09:53:42 -0800145 _POOL_SET = ['humpty', 'dumpty']
146
J. Richard Barnette96db3492015-03-27 17:23:52 -0700147 def setUp(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800148 super(PoolSetInventoryTestCase, self).setUp()
149 self._pool_histories = lab_inventory._PoolSetInventory(self._POOL_SET)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700150
J. Richard Barnette96db3492015-03-27 17:23:52 -0700151 def _add_host(self, pool, status):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800152 fake = _FakeHostHistory('zebra', pool, status)
153 self._pool_histories.record_host(fake)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700154
J. Richard Barnette96db3492015-03-27 17:23:52 -0700155 def _check_all_counts(self, working, broken):
156 """Check that total counts for all pools match expectations.
157
158 Checks that `get_working()` and `get_broken()` return the
159 given expected values when called without a pool specified.
160 Also check that `get_total()` is the sum of working and
161 broken devices.
162
163 Additionally, call the various functions for all the pools
164 individually, and confirm that the totals across pools match
165 the given expectations.
166
167 @param working The expected total of working devices.
168 @param broken The expected total of broken devices.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700169 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800170 self.assertEqual(self._pool_histories.get_working(), working)
171 self.assertEqual(self._pool_histories.get_broken(), broken)
172 self.assertEqual(self._pool_histories.get_total(),
J. Richard Barnette96db3492015-03-27 17:23:52 -0700173 working + broken)
174 count_working = 0
175 count_broken = 0
176 count_total = 0
Richard Barnette5de01eb2017-12-15 09:53:42 -0800177 for pool in self._POOL_SET:
178 count_working += self._pool_histories.get_working(pool)
179 count_broken += self._pool_histories.get_broken(pool)
180 count_total += self._pool_histories.get_total(pool)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700181 self.assertEqual(count_working, working)
182 self.assertEqual(count_broken, broken)
183 self.assertEqual(count_total, working + broken)
184
J. Richard Barnette96db3492015-03-27 17:23:52 -0700185 def _check_pool_counts(self, pool, working, broken):
186 """Check that counts for a given pool match expectations.
187
188 Checks that `get_working()` and `get_broken()` return the
189 given expected values for the given pool. Also check that
190 `get_total()` is the sum of working and broken devices.
191
192 @param pool The pool to be checked.
193 @param working The expected total of working devices.
194 @param broken The expected total of broken devices.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700195 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800196 self.assertEqual(self._pool_histories.get_working(pool),
J. Richard Barnette96db3492015-03-27 17:23:52 -0700197 working)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800198 self.assertEqual(self._pool_histories.get_broken(pool),
J. Richard Barnette96db3492015-03-27 17:23:52 -0700199 broken)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800200 self.assertEqual(self._pool_histories.get_total(pool),
J. Richard Barnette96db3492015-03-27 17:23:52 -0700201 working + broken)
202
J. Richard Barnette96db3492015-03-27 17:23:52 -0700203 def test_empty(self):
204 """Test counts when there are no DUTs recorded."""
205 self._check_all_counts(0, 0)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800206 for pool in self._POOL_SET:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700207 self._check_pool_counts(pool, 0, 0)
208
J. Richard Barnette96db3492015-03-27 17:23:52 -0700209 def test_all_working_then_broken(self):
210 """Test counts after adding a working and then a broken DUT.
211
212 For each pool, add first a working, then a broken DUT. After
213 each DUT is added, check counts to confirm the correct values.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700214 """
215 working = 0
216 broken = 0
Richard Barnette5de01eb2017-12-15 09:53:42 -0800217 for pool in self._POOL_SET:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700218 self._add_host(pool, _WORKING)
219 working += 1
220 self._check_pool_counts(pool, 1, 0)
221 self._check_all_counts(working, broken)
222 self._add_host(pool, _BROKEN)
223 broken += 1
224 self._check_pool_counts(pool, 1, 1)
225 self._check_all_counts(working, broken)
226
J. Richard Barnette96db3492015-03-27 17:23:52 -0700227 def test_all_broken_then_working(self):
228 """Test counts after adding a broken and then a working DUT.
229
230 For each pool, add first a broken, then a working DUT. After
231 each DUT is added, check counts to confirm the correct values.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700232 """
233 working = 0
234 broken = 0
Richard Barnette5de01eb2017-12-15 09:53:42 -0800235 for pool in self._POOL_SET:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700236 self._add_host(pool, _BROKEN)
237 broken += 1
238 self._check_pool_counts(pool, 0, 1)
239 self._check_all_counts(working, broken)
240 self._add_host(pool, _WORKING)
241 working += 1
242 self._check_pool_counts(pool, 1, 1)
243 self._check_all_counts(working, broken)
244
245
J. Richard Barnettef6839282015-06-01 16:00:35 -0700246class LocationSortTests(unittest.TestCase):
247 """Unit tests for `_sort_by_location()`."""
248
249 def setUp(self):
250 super(LocationSortTests, self).setUp()
251
J. Richard Barnettef6839282015-06-01 16:00:35 -0700252 def _check_sorting(self, *locations):
253 """Test sorting a given list of locations.
254
255 The input is an already ordered list of lists of tuples with
256 row, rack, and host numbers. The test converts the tuples
257 to hostnames, preserving the original ordering. Then it
258 flattens and scrambles the input, runs it through
259 `_sort_by_location()`, and asserts that the result matches
260 the original.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700261 """
262 lab = 0
263 expected = []
264 for tuples in locations:
265 lab += 1
266 expected.append(
267 [_FakeHostLocation((lab,) + t) for t in tuples])
268 scrambled = [e for e in itertools.chain(*expected)]
269 scrambled = [e for e in reversed(scrambled)]
270 actual = lab_inventory._sort_by_location(scrambled)
271 # The ordering of the labs in the output isn't guaranteed,
272 # so we can't compare `expected` and `actual` directly.
273 # Instead, we create a dictionary keyed on the first host in
274 # each lab, and compare the dictionaries.
275 self.assertEqual({l[0]: l for l in expected},
276 {l[0]: l for l in actual})
277
J. Richard Barnettef6839282015-06-01 16:00:35 -0700278 def test_separate_labs(self):
279 """Test that sorting distinguishes labs."""
280 self._check_sorting([(1, 1, 1)], [(1, 1, 1)], [(1, 1, 1)])
281
J. Richard Barnettef6839282015-06-01 16:00:35 -0700282 def test_separate_rows(self):
283 """Test for proper sorting when only rows are different."""
284 self._check_sorting([(1, 1, 1), (9, 1, 1), (10, 1, 1)])
285
J. Richard Barnettef6839282015-06-01 16:00:35 -0700286 def test_separate_racks(self):
287 """Test for proper sorting when only racks are different."""
288 self._check_sorting([(1, 1, 1), (1, 9, 1), (1, 10, 1)])
289
J. Richard Barnettef6839282015-06-01 16:00:35 -0700290 def test_separate_hosts(self):
291 """Test for proper sorting when only hosts are different."""
292 self._check_sorting([(1, 1, 1), (1, 1, 9), (1, 1, 10)])
293
J. Richard Barnettef6839282015-06-01 16:00:35 -0700294 def test_diagonal(self):
295 """Test for proper sorting when all parts are different."""
296 self._check_sorting([(1, 1, 2), (1, 2, 1), (2, 1, 1)])
297
298
299class InventoryScoringTests(unittest.TestCase):
300 """Unit tests for `_score_repair_set()`."""
301
302 def setUp(self):
303 super(InventoryScoringTests, self).setUp()
304
J. Richard Barnettef6839282015-06-01 16:00:35 -0700305 def _make_buffer_counts(self, *counts):
306 """Create a dictionary suitable as `buffer_counts`.
307
Richard Barnette5de01eb2017-12-15 09:53:42 -0800308 @param counts List of tuples with model count data.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700309 """
310 self._buffer_counts = dict(counts)
311
J. Richard Barnettef6839282015-06-01 16:00:35 -0700312 def _make_history_list(self, repair_counts):
313 """Create a list suitable as `repair_list`.
314
Richard Barnette5de01eb2017-12-15 09:53:42 -0800315 @param repair_counts List of (model, count) tuples.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700316 """
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700317 pool = lab_inventory.SPARE_POOL
J. Richard Barnettef6839282015-06-01 16:00:35 -0700318 histories = []
Richard Barnette5de01eb2017-12-15 09:53:42 -0800319 for model, count in repair_counts:
J. Richard Barnettef6839282015-06-01 16:00:35 -0700320 for i in range(0, count):
321 histories.append(
Richard Barnette5de01eb2017-12-15 09:53:42 -0800322 _FakeHostHistory(model, pool, _BROKEN))
J. Richard Barnettef6839282015-06-01 16:00:35 -0700323 return histories
324
J. Richard Barnettef6839282015-06-01 16:00:35 -0700325 def _check_better(self, repair_a, repair_b):
326 """Test that repair set A scores better than B.
327
328 Contruct repair sets from `repair_a` and `repair_b`,
329 and score both of them using the pre-existing
330 `self._buffer_counts`. Assert that the score for A is
331 better than the score for B.
332
333 @param repair_a Input data for repair set A
334 @param repair_b Input data for repair set B
J. Richard Barnettef6839282015-06-01 16:00:35 -0700335 """
336 score_a = lab_inventory._score_repair_set(
337 self._buffer_counts,
338 self._make_history_list(repair_a))
339 score_b = lab_inventory._score_repair_set(
340 self._buffer_counts,
341 self._make_history_list(repair_b))
342 self.assertGreater(score_a, score_b)
343
J. Richard Barnettef6839282015-06-01 16:00:35 -0700344 def _check_equal(self, repair_a, repair_b):
345 """Test that repair set A scores the same as B.
346
347 Contruct repair sets from `repair_a` and `repair_b`,
348 and score both of them using the pre-existing
349 `self._buffer_counts`. Assert that the score for A is
350 equal to the score for B.
351
352 @param repair_a Input data for repair set A
353 @param repair_b Input data for repair set B
J. Richard Barnettef6839282015-06-01 16:00:35 -0700354 """
355 score_a = lab_inventory._score_repair_set(
356 self._buffer_counts,
357 self._make_history_list(repair_a))
358 score_b = lab_inventory._score_repair_set(
359 self._buffer_counts,
360 self._make_history_list(repair_b))
361 self.assertEqual(score_a, score_b)
362
Richard Barnette5de01eb2017-12-15 09:53:42 -0800363 def test_improve_worst_model(self):
364 """Test that improving the worst model improves scoring.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700365
Richard Barnette5de01eb2017-12-15 09:53:42 -0800366 Construct a buffer counts dictionary with all models having
J. Richard Barnettef6839282015-06-01 16:00:35 -0700367 different counts. Assert that it is both necessary and
Richard Barnette5de01eb2017-12-15 09:53:42 -0800368 sufficient to improve the count of the worst model in order
J. Richard Barnettef6839282015-06-01 16:00:35 -0700369 to improve the score.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700370 """
371 self._make_buffer_counts(('lion', 0),
372 ('tiger', 1),
373 ('bear', 2))
374 self._check_better([('lion', 1)], [('tiger', 1)])
375 self._check_better([('lion', 1)], [('bear', 1)])
376 self._check_better([('lion', 1)], [('tiger', 2)])
377 self._check_better([('lion', 1)], [('bear', 2)])
378 self._check_equal([('tiger', 1)], [('bear', 1)])
379
J. Richard Barnettef6839282015-06-01 16:00:35 -0700380 def test_improve_worst_case_count(self):
381 """Test that improving the number of worst cases improves the score.
382
Richard Barnette5de01eb2017-12-15 09:53:42 -0800383 Construct a buffer counts dictionary with all models having
384 the same counts. Assert that improving two models is better
385 than improving one. Assert that improving any one model is
J. Richard Barnettef6839282015-06-01 16:00:35 -0700386 as good as any other.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700387 """
388 self._make_buffer_counts(('lion', 0),
389 ('tiger', 0),
390 ('bear', 0))
391 self._check_better([('lion', 1), ('tiger', 1)], [('bear', 2)])
392 self._check_equal([('lion', 2)], [('tiger', 1)])
393 self._check_equal([('tiger', 1)], [('bear', 1)])
394
395
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800396# Each item is the number of DUTs in that status.
397STATUS_CHOICES = (_WORKING, _BROKEN, _UNUSED)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800398StatusCounts = collections.namedtuple('StatusCounts', ['good', 'bad', 'idle'])
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800399# Each item is a StatusCounts tuple specifying the number of DUTs per status in
400# the that pool.
401CRITICAL_POOL = lab_inventory.CRITICAL_POOLS[0]
402SPARE_POOL = lab_inventory.SPARE_POOL
403POOL_CHOICES = (CRITICAL_POOL, SPARE_POOL)
404PoolStatusCounts = collections.namedtuple('PoolStatusCounts',
405 ['critical', 'spare'])
J. Richard Barnette96db3492015-03-27 17:23:52 -0700406
Richard Barnette5de01eb2017-12-15 09:53:42 -0800407def create_inventory(data):
408 """Create a `_LabInventory` instance for testing.
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800409
Richard Barnette5de01eb2017-12-15 09:53:42 -0800410 This function allows the construction of a complete `_LabInventory`
411 object from a simplified input representation.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700412
413 A single 'critical pool' is arbitrarily chosen for purposes of
414 testing; there's no coverage for testing arbitrary combinations
415 in more than one critical pool.
416
Richard Barnette5de01eb2017-12-15 09:53:42 -0800417 @param data: dict {key: PoolStatusCounts}.
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800418 @returns: lab_inventory._LabInventory object.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700419 """
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800420 histories = []
Richard Barnette5de01eb2017-12-15 09:53:42 -0800421 for model, counts in data.iteritems():
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800422 for p, pool in enumerate(POOL_CHOICES):
423 for s, status in enumerate(STATUS_CHOICES):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800424 fake_host = _FakeHostHistory(model, pool, status)
425 histories.extend([fake_host] * counts[p][s])
426 inventory = lab_inventory._LabInventory(
427 histories, lab_inventory.MANAGED_POOLS)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800428 return inventory
J. Richard Barnette96db3492015-03-27 17:23:52 -0700429
Richard Barnette5de01eb2017-12-15 09:53:42 -0800430
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800431class LabInventoryTests(unittest.TestCase):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700432 """Tests for the basic functions of `_LabInventory`.
433
Richard Barnette5de01eb2017-12-15 09:53:42 -0800434 Contains basic coverage to show that after an inventory is created
435 and DUTs with known status are added, the inventory counts match the
436 counts of the added DUTs.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700437 """
438
Richard Barnette5de01eb2017-12-15 09:53:42 -0800439 _MODEL_LIST = ['lion', 'tiger', 'bear'] # Oh, my!
J. Richard Barnette96db3492015-03-27 17:23:52 -0700440
Richard Barnette5de01eb2017-12-15 09:53:42 -0800441 def _check_inventory_counts(self, inventory, data, msg=None):
442 """Check that all counts in the inventory match `data`.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700443
Richard Barnette5de01eb2017-12-15 09:53:42 -0800444 This asserts that the actual counts returned by the various
445 accessor functions for `inventory` match the values expected for
446 the given `data` that created the inventory.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700447
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800448 @param inventory: _LabInventory object to check.
449 @param data Inventory data to check against. Same type as
450 `create_inventory`.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700451 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800452 self.assertEqual(set(inventory.keys()), set(data.keys()))
453 for model, histories in inventory.iteritems():
454 expected_counts = data[model]
455 actual_counts = PoolStatusCounts(
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800456 StatusCounts(
457 histories.get_working(CRITICAL_POOL),
458 histories.get_broken(CRITICAL_POOL),
459 histories.get_idle(CRITICAL_POOL),
460 ),
461 StatusCounts(
462 histories.get_working(SPARE_POOL),
463 histories.get_broken(SPARE_POOL),
464 histories.get_idle(SPARE_POOL),
465 ),
466 )
Richard Barnette5de01eb2017-12-15 09:53:42 -0800467 self.assertEqual(actual_counts, expected_counts, msg)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800468
469 self.assertEqual(len(histories.get_working_list()),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800470 sum([p.good for p in expected_counts]),
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800471 msg)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800472 self.assertEqual(len(histories.get_broken_list()),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800473 sum([p.bad for p in expected_counts]),
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800474 msg)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800475 self.assertEqual(len(histories.get_idle_list()),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800476 sum([p.idle for p in expected_counts]),
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800477 msg)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700478
J. Richard Barnette96db3492015-03-27 17:23:52 -0700479 def test_empty(self):
480 """Test counts when there are no DUTs recorded."""
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800481 inventory = create_inventory({})
482 self.assertEqual(inventory.get_num_duts(), 0)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800483 self.assertEqual(inventory.get_boards(), set())
484 self._check_inventory_counts(inventory, {})
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800485 self.assertEqual(inventory.get_num_models(), 0)
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700486
Richard Barnette5de01eb2017-12-15 09:53:42 -0800487 def _check_model_count(self, model_count):
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800488 """Parameterized test for testing a specific number of models."""
Richard Barnette5de01eb2017-12-15 09:53:42 -0800489 msg = '[model: %d]' % (model_count,)
490 models = self._MODEL_LIST[:model_count]
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800491 data = {
492 m: PoolStatusCounts(
493 StatusCounts(1, 1, 1),
494 StatusCounts(1, 1, 1),
495 )
496 for m in models
497 }
Richard Barnette5de01eb2017-12-15 09:53:42 -0800498 inventory = create_inventory(data)
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800499 self.assertEqual(inventory.get_num_duts(), 6 * model_count, msg)
500 self.assertEqual(inventory.get_num_models(), model_count, msg)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800501 for pool in [CRITICAL_POOL, SPARE_POOL]:
502 self.assertEqual(set(inventory.get_pool_models(pool)),
503 set(models))
504 self._check_inventory_counts(inventory, data, msg=msg)
505
Richard Barnette5de01eb2017-12-15 09:53:42 -0800506 def test_model_counts(self):
507 """Test counts for various numbers of models."""
508 self.longMessage = True
509 for model_count in range(0, len(self._MODEL_LIST)):
510 self._check_model_count(model_count)
511
Richard Barnette5de01eb2017-12-15 09:53:42 -0800512 def _check_single_dut_counts(self, critical, spare):
513 """Parmeterized test for single dut counts."""
514 self.longMessage = True
515 counts = PoolStatusCounts(critical, spare)
516 model = self._MODEL_LIST[0]
517 data = {model: counts}
518 msg = '[data: %s]' % (data,)
519 inventory = create_inventory(data)
520 self.assertEqual(inventory.get_num_duts(), 1, msg)
521 self.assertEqual(inventory.get_num_models(), 1, msg)
522 self._check_inventory_counts(inventory, data, msg=msg)
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800523
J. Richard Barnette96db3492015-03-27 17:23:52 -0700524 def test_single_dut_counts(self):
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800525 """Test counts when there is a single DUT per board, and it is good."""
Richard Barnette5de01eb2017-12-15 09:53:42 -0800526 status_100 = StatusCounts(1, 0, 0)
527 status_010 = StatusCounts(0, 1, 0)
528 status_001 = StatusCounts(0, 0, 1)
529 status_null = StatusCounts(0, 0, 0)
530 self._check_single_dut_counts(status_100, status_null)
531 self._check_single_dut_counts(status_010, status_null)
532 self._check_single_dut_counts(status_001, status_null)
533 self._check_single_dut_counts(status_null, status_100)
534 self._check_single_dut_counts(status_null, status_010)
535 self._check_single_dut_counts(status_null, status_001)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700536
537
Richard Barnette5de01eb2017-12-15 09:53:42 -0800538# MODEL_MESSAGE_TEMPLATE -
J. Richard Barnette96db3492015-03-27 17:23:52 -0700539# This is a sample of the output text produced by
Richard Barnette5de01eb2017-12-15 09:53:42 -0800540# _generate_model_inventory_message(). This string is parsed by the
J. Richard Barnette96db3492015-03-27 17:23:52 -0700541# tests below to construct a sample inventory that should produce
542# the output, and then the output is generated and checked against
543# this original sample.
544#
545# Constructing inventories from parsed sample text serves two
546# related purposes:
547# - It provides a way to see what the output should look like
548# without having to run the script.
549# - It helps make sure that a human being will actually look at
550# the output to see that it's basically readable.
551# This should also help prevent test bugs caused by writing tests
552# that simply parrot the original output generation code.
553
Richard Barnette5de01eb2017-12-15 09:53:42 -0800554_MODEL_MESSAGE_TEMPLATE = '''
555Model Avail Bad Idle Good Spare Total
xixuan12ce04f2016-03-10 13:16:30 -0800556lion -1 13 2 11 12 26
557tiger -1 5 2 9 4 16
558bear 0 5 2 10 5 17
559platypus 4 2 2 20 6 24
560aardvark 7 2 2 6 9 10
J. Richard Barnette96db3492015-03-27 17:23:52 -0700561'''
562
563
Richard Barnette5de01eb2017-12-15 09:53:42 -0800564class PoolSetInventoryTests(unittest.TestCase):
565 """Tests for `_generate_model_inventory_message()`.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700566
567 The tests create various test inventories designed to match the
Richard Barnette5de01eb2017-12-15 09:53:42 -0800568 counts in `_MODEL_MESSAGE_TEMPLATE`, and asserts that the
J. Richard Barnette96db3492015-03-27 17:23:52 -0700569 generated message text matches the original message text.
570
571 Message text is represented as a list of strings, split on the
572 `'\n'` separator.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700573 """
574
575 def setUp(self):
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800576 self.maxDiff = None
Richard Barnette5de01eb2017-12-15 09:53:42 -0800577 lines = [x.strip() for x in _MODEL_MESSAGE_TEMPLATE.split('\n') if
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800578 x.strip()]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800579 self._header, self._model_lines = lines[0], lines[1:]
580 self._model_data = []
581 for l in self._model_lines:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700582 items = l.split()
Richard Barnette5de01eb2017-12-15 09:53:42 -0800583 model = items[0]
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800584 bad, idle, good, spare = [int(x) for x in items[2:-1]]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800585 self._model_data.append((model, (good, bad, idle, spare)))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700586
J. Richard Barnette96db3492015-03-27 17:23:52 -0700587 def _make_minimum_spares(self, counts):
588 """Create a counts tuple with as few spare DUTs as possible."""
xixuan12ce04f2016-03-10 13:16:30 -0800589 good, bad, idle, spares = counts
590 if spares > bad + idle:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800591 return PoolStatusCounts(
592 StatusCounts(good + bad +idle - spares, 0, 0),
593 StatusCounts(spares - bad - idle, bad, idle),
594 )
xixuan12ce04f2016-03-10 13:16:30 -0800595 elif spares < bad:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800596 return PoolStatusCounts(
597 StatusCounts(good, bad - spares, idle),
598 StatusCounts(0, spares, 0),
599 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700600 else:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800601 return PoolStatusCounts(
602 StatusCounts(good, 0, idle + bad - spares),
603 StatusCounts(0, bad, spares - bad),
604 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700605
J. Richard Barnette96db3492015-03-27 17:23:52 -0700606 def _make_maximum_spares(self, counts):
607 """Create a counts tuple with as many spare DUTs as possible."""
xixuan12ce04f2016-03-10 13:16:30 -0800608 good, bad, idle, spares = counts
J. Richard Barnette96db3492015-03-27 17:23:52 -0700609 if good > spares:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800610 return PoolStatusCounts(
611 StatusCounts(good - spares, bad, idle),
612 StatusCounts(spares, 0, 0),
613 )
xixuan12ce04f2016-03-10 13:16:30 -0800614 elif good + bad > spares:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800615 return PoolStatusCounts(
616 StatusCounts(0, good + bad - spares, idle),
617 StatusCounts(good, spares - good, 0),
618 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700619 else:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800620 return PoolStatusCounts(
621 StatusCounts(0, 0, good + bad + idle - spares),
622 StatusCounts(good, bad, spares - good - bad),
623 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700624
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800625 def _check_message(self, message):
626 """Checks that message approximately matches expected string."""
627 message = [x.strip() for x in message.split('\n') if x.strip()]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700628 self.assertIn(self._header, message)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800629 body = message[message.index(self._header) + 1:]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800630 self.assertEqual(body, self._model_lines)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700631
J. Richard Barnette96db3492015-03-27 17:23:52 -0700632 def test_minimum_spares(self):
633 """Test message generation when the spares pool is low."""
634 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800635 model: self._make_minimum_spares(counts)
636 for model, counts in self._model_data
J. Richard Barnette96db3492015-03-27 17:23:52 -0700637 }
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800638 inventory = create_inventory(data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800639 message = lab_inventory._generate_model_inventory_message(inventory)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800640 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700641
642 def test_maximum_spares(self):
643 """Test message generation when the critical pool is low."""
644 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800645 model: self._make_maximum_spares(counts)
646 for model, counts in self._model_data
J. Richard Barnette96db3492015-03-27 17:23:52 -0700647 }
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800648 inventory = create_inventory(data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800649 message = lab_inventory._generate_model_inventory_message(inventory)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800650 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700651
J. Richard Barnette96db3492015-03-27 17:23:52 -0700652 def test_ignore_no_spares(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800653 """Test that messages ignore models with no spare pool."""
J. Richard Barnette96db3492015-03-27 17:23:52 -0700654 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800655 model: self._make_maximum_spares(counts)
656 for model, counts in self._model_data
J. Richard Barnette96db3492015-03-27 17:23:52 -0700657 }
xixuan12ce04f2016-03-10 13:16:30 -0800658 data['elephant'] = ((5, 4, 0), (0, 0, 0))
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800659 inventory = create_inventory(data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800660 message = lab_inventory._generate_model_inventory_message(inventory)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800661 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700662
J. Richard Barnette96db3492015-03-27 17:23:52 -0700663 def test_ignore_no_critical(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800664 """Test that messages ignore models with no critical pools."""
J. Richard Barnette96db3492015-03-27 17:23:52 -0700665 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800666 model: self._make_maximum_spares(counts)
667 for model, counts in self._model_data
J. Richard Barnette96db3492015-03-27 17:23:52 -0700668 }
xixuan12ce04f2016-03-10 13:16:30 -0800669 data['elephant'] = ((0, 0, 0), (1, 5, 1))
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800670 inventory = create_inventory(data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800671 message = lab_inventory._generate_model_inventory_message(inventory)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800672 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700673
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800674 def test_ignore_no_bad(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800675 """Test that messages ignore models with no bad DUTs."""
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800676 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800677 model: self._make_maximum_spares(counts)
678 for model, counts in self._model_data
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800679 }
xixuan12ce04f2016-03-10 13:16:30 -0800680 data['elephant'] = ((5, 0, 1), (5, 0, 1))
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800681 inventory = create_inventory(data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800682 message = lab_inventory._generate_model_inventory_message(inventory)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800683 self._check_message(message)
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800684
685
xixuan12ce04f2016-03-10 13:16:30 -0800686class _PoolInventoryTestBase(unittest.TestCase):
687 """Parent class for tests relating to generating pool inventory messages.
688
689 Func `setUp` in the class parses a given |message_template| to obtain
690 header and body.
691 """
Richard Barnettedf01f1b2018-04-20 14:44:40 -0400692
xixuan12ce04f2016-03-10 13:16:30 -0800693 def _read_template(self, message_template):
694 """Read message template for PoolInventoryTest and IdleInventoryTest.
695
696 @param message_template: the input template to be parsed into: header
697 and content (report_lines).
xixuan12ce04f2016-03-10 13:16:30 -0800698 """
699 message_lines = message_template.split('\n')
700 self._header = message_lines[1]
701 self._report_lines = message_lines[2:-1]
702
xixuan12ce04f2016-03-10 13:16:30 -0800703 def _check_report_no_info(self, text):
704 """Test a message body containing no reported info.
705
Richard Barnettedf01f1b2018-04-20 14:44:40 -0400706 The input `text` was created from a query to an inventory, which
707 has no objects meet the query and leads to an `empty` return.
708 Assert that the text consists of a single line starting with '('
709 and ending with ')'.
xixuan12ce04f2016-03-10 13:16:30 -0800710
711 @param text: Message body text to be tested.
xixuan12ce04f2016-03-10 13:16:30 -0800712 """
713 self.assertTrue(len(text) == 1 and
714 text[0][0] == '(' and
715 text[0][-1] == ')')
716
xixuan12ce04f2016-03-10 13:16:30 -0800717 def _check_report(self, text):
718 """Test a message against the passed |expected_content|.
719
720 @param text: Message body text to be tested.
721 @param expected_content: The ground-truth content to be compared with.
xixuan12ce04f2016-03-10 13:16:30 -0800722 """
723 self.assertEqual(text, self._report_lines)
724
725
J. Richard Barnette96db3492015-03-27 17:23:52 -0700726# _POOL_MESSAGE_TEMPLATE -
727# This is a sample of the output text produced by
728# _generate_pool_inventory_message(). This string is parsed by the
729# tests below to construct a sample inventory that should produce
730# the output, and then the output is generated and checked against
731# this original sample.
732#
733# See the comments on _BOARD_MESSAGE_TEMPLATE above for the
734# rationale on using sample text in this way.
735
736_POOL_MESSAGE_TEMPLATE = '''
Richard Barnette5de01eb2017-12-15 09:53:42 -0800737Model Bad Idle Good Total
xixuan12ce04f2016-03-10 13:16:30 -0800738lion 5 2 6 13
739tiger 4 1 5 10
740bear 3 0 7 10
741aardvark 2 0 0 2
742platypus 1 1 1 3
J. Richard Barnette96db3492015-03-27 17:23:52 -0700743'''
744
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700745_POOL_ADMIN_URL = 'http://go/cros-manage-duts'
746
747
xixuan12ce04f2016-03-10 13:16:30 -0800748class PoolInventoryTests(_PoolInventoryTestBase):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700749 """Tests for `_generate_pool_inventory_message()`.
750
751 The tests create various test inventories designed to match the
752 counts in `_POOL_MESSAGE_TEMPLATE`, and assert that the
753 generated message text matches the format established in the
754 original message text.
755
756 The output message text is parsed against the following grammar:
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700757 <message> -> <intro> <pool> { "blank line" <pool> }
758 <intro> ->
759 Instructions to depty mentioning the admin page URL
760 A blank line
J. Richard Barnette96db3492015-03-27 17:23:52 -0700761 <pool> ->
762 <description>
763 <header line>
764 <message body>
765 <description> ->
766 Any number of lines describing one pool
767 <header line> ->
768 The header line from `_POOL_MESSAGE_TEMPLATE`
769 <message body> ->
770 Any number of non-blank lines
771
772 After parsing messages into the parts described above, various
773 assertions are tested against the parsed output, including
774 that the message body matches the body from
775 `_POOL_MESSAGE_TEMPLATE`.
776
777 Parse message text is represented as a list of strings, split on
778 the `'\n'` separator.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700779 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800780
J. Richard Barnette96db3492015-03-27 17:23:52 -0700781 def setUp(self):
xixuan12ce04f2016-03-10 13:16:30 -0800782 super(PoolInventoryTests, self)._read_template(_POOL_MESSAGE_TEMPLATE)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800783 self._model_data = []
xixuan12ce04f2016-03-10 13:16:30 -0800784 for l in self._report_lines:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700785 items = l.split()
Richard Barnette5de01eb2017-12-15 09:53:42 -0800786 model = items[0]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700787 bad = int(items[1])
xixuan12ce04f2016-03-10 13:16:30 -0800788 idle = int(items[2])
789 good = int(items[3])
Richard Barnette5de01eb2017-12-15 09:53:42 -0800790 self._model_data.append((model, (good, bad, idle)))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700791
Richard Barnette5de01eb2017-12-15 09:53:42 -0800792 def _create_histories(self, pools, model_data):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700793 """Return a list suitable to create a `_LabInventory` object.
794
795 Creates a list of `_FakeHostHistory` objects that can be
796 used to create a lab inventory. `pools` is a list of strings
Richard Barnette5de01eb2017-12-15 09:53:42 -0800797 naming pools, and `model_data` is a list of tuples of the
J. Richard Barnette96db3492015-03-27 17:23:52 -0700798 form
Richard Barnette5de01eb2017-12-15 09:53:42 -0800799 `(model, (goodcount, badcount))`
J. Richard Barnette96db3492015-03-27 17:23:52 -0700800 where
Richard Barnette5de01eb2017-12-15 09:53:42 -0800801 `model` is a model name.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700802 `goodcount` is the number of working DUTs in the pool.
803 `badcount` is the number of broken DUTs in the pool.
804
805 @param pools List of pools for which to create
806 histories.
Richard Barnette5de01eb2017-12-15 09:53:42 -0800807 @param model_data List of tuples containing models and DUT
J. Richard Barnette96db3492015-03-27 17:23:52 -0700808 counts.
809 @return A list of `_FakeHostHistory` objects that can be
810 used to create a `_LabInventory` object.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700811 """
812 histories = []
xixuan12ce04f2016-03-10 13:16:30 -0800813 status_choices = (_WORKING, _BROKEN, _UNUSED)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700814 for pool in pools:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800815 for model, counts in model_data:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700816 for status, count in zip(status_choices, counts):
817 for x in range(0, count):
818 histories.append(
Richard Barnette5de01eb2017-12-15 09:53:42 -0800819 _FakeHostHistory(model, pool, status))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700820 return histories
821
J. Richard Barnette96db3492015-03-27 17:23:52 -0700822 def _parse_pool_summaries(self, histories):
823 """Parse message output according to the grammar above.
824
825 Create a lab inventory from the given `histories`, and
826 generate the pool inventory message. Then parse the message
827 and return a dictionary mapping each pool to the message
828 body parsed after that pool.
829
830 Tests the following assertions:
831 * Each <description> contains a mention of exactly one
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700832 pool in the `CRITICAL_POOLS` list.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700833 * Each pool is mentioned in exactly one <description>.
834 Note that the grammar requires the header to appear once
835 for each pool, so the parsing implicitly asserts that the
836 output contains the header.
837
838 @param histories Input used to create the test
839 `_LabInventory` object.
Richard Barnette5de01eb2017-12-15 09:53:42 -0800840 @return A dictionary mapping model names to the output
841 (a list of lines) for the model.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700842 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800843 inventory = lab_inventory._LabInventory(
844 histories, lab_inventory.MANAGED_POOLS)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700845 message = lab_inventory._generate_pool_inventory_message(
xixuan12ce04f2016-03-10 13:16:30 -0800846 inventory).split('\n')
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700847 poolset = set(lab_inventory.CRITICAL_POOLS)
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700848 seen_url = False
849 seen_intro = False
J. Richard Barnette96db3492015-03-27 17:23:52 -0700850 description = ''
Richard Barnette5de01eb2017-12-15 09:53:42 -0800851 model_text = {}
J. Richard Barnette96db3492015-03-27 17:23:52 -0700852 current_pool = None
853 for line in message:
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700854 if not seen_url:
855 if _POOL_ADMIN_URL in line:
856 seen_url = True
857 elif not seen_intro:
858 if not line:
859 seen_intro = True
860 elif current_pool is None:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700861 if line == self._header:
862 pools_mentioned = [p for p in poolset
863 if p in description]
864 self.assertEqual(len(pools_mentioned), 1)
865 current_pool = pools_mentioned[0]
866 description = ''
Richard Barnette5de01eb2017-12-15 09:53:42 -0800867 model_text[current_pool] = []
J. Richard Barnette96db3492015-03-27 17:23:52 -0700868 poolset.remove(current_pool)
869 else:
870 description += line
871 else:
872 if line:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800873 model_text[current_pool].append(line)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700874 else:
875 current_pool = None
876 self.assertEqual(len(poolset), 0)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800877 return model_text
J. Richard Barnette96db3492015-03-27 17:23:52 -0700878
J. Richard Barnette96db3492015-03-27 17:23:52 -0700879 def test_no_shortages(self):
880 """Test correct output when no pools have shortages."""
Richard Barnette5de01eb2017-12-15 09:53:42 -0800881 model_text = self._parse_pool_summaries([])
882 for text in model_text.values():
xixuan12ce04f2016-03-10 13:16:30 -0800883 self._check_report_no_info(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700884
J. Richard Barnette96db3492015-03-27 17:23:52 -0700885 def test_one_pool_shortage(self):
886 """Test correct output when exactly one pool has a shortage."""
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700887 for pool in lab_inventory.CRITICAL_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700888 histories = self._create_histories((pool,),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800889 self._model_data)
890 model_text = self._parse_pool_summaries(histories)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700891 for checkpool in lab_inventory.CRITICAL_POOLS:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800892 text = model_text[checkpool]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700893 if checkpool == pool:
xixuan12ce04f2016-03-10 13:16:30 -0800894 self._check_report(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700895 else:
xixuan12ce04f2016-03-10 13:16:30 -0800896 self._check_report_no_info(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700897
J. Richard Barnette96db3492015-03-27 17:23:52 -0700898 def test_all_pool_shortages(self):
899 """Test correct output when all pools have a shortage."""
900 histories = []
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700901 for pool in lab_inventory.CRITICAL_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700902 histories.extend(
903 self._create_histories((pool,),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800904 self._model_data))
905 model_text = self._parse_pool_summaries(histories)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700906 for pool in lab_inventory.CRITICAL_POOLS:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800907 self._check_report(model_text[pool])
J. Richard Barnette96db3492015-03-27 17:23:52 -0700908
Richard Barnette5de01eb2017-12-15 09:53:42 -0800909 def test_full_model_ignored(self):
910 """Test that models at full strength are not reported."""
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700911 pool = lab_inventory.CRITICAL_POOLS[0]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800912 full_model = [('echidna', (5, 0, 0))]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700913 histories = self._create_histories((pool,),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800914 full_model)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700915 text = self._parse_pool_summaries(histories)[pool]
xixuan12ce04f2016-03-10 13:16:30 -0800916 self._check_report_no_info(text)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800917 model_data = self._model_data + full_model
918 histories = self._create_histories((pool,), model_data)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700919 text = self._parse_pool_summaries(histories)[pool]
xixuan12ce04f2016-03-10 13:16:30 -0800920 self._check_report(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700921
J. Richard Barnette96db3492015-03-27 17:23:52 -0700922 def test_spare_pool_ignored(self):
923 """Test that reporting ignores the spare pool inventory."""
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700924 spare_pool = lab_inventory.SPARE_POOL
Richard Barnette5de01eb2017-12-15 09:53:42 -0800925 spare_data = self._model_data + [('echidna', (0, 5, 0))]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700926 histories = self._create_histories((spare_pool,),
927 spare_data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800928 model_text = self._parse_pool_summaries(histories)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700929 for pool in lab_inventory.CRITICAL_POOLS:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800930 self._check_report_no_info(model_text[pool])
xixuan12ce04f2016-03-10 13:16:30 -0800931
932
933_IDLE_MESSAGE_TEMPLATE = '''
Richard Barnette5de01eb2017-12-15 09:53:42 -0800934Hostname Model Pool
xixuan12ce04f2016-03-10 13:16:30 -0800935chromeos4-row12-rack4-host7 tiger bvt
936chromeos1-row3-rack1-host2 lion bvt
937chromeos3-row2-rack2-host5 lion cq
938chromeos2-row7-rack3-host11 platypus suites
939'''
940
941
942class IdleInventoryTests(_PoolInventoryTestBase):
943 """Tests for `_generate_idle_inventory_message()`.
944
945 The tests create idle duts that match the counts and pool in
946 `_IDLE_MESSAGE_TEMPLATE`. In test, it asserts that the generated
947 idle message text matches the format established in
948 `_IDLE_MESSAGE_TEMPLATE`.
949
950 Parse message text is represented as a list of strings, split on
951 the `'\n'` separator.
xixuan12ce04f2016-03-10 13:16:30 -0800952 """
953
954 def setUp(self):
955 super(IdleInventoryTests, self)._read_template(_IDLE_MESSAGE_TEMPLATE)
956 self._host_data = []
957 for h in self._report_lines:
958 items = h.split()
959 hostname = items[0]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800960 model = items[1]
xixuan12ce04f2016-03-10 13:16:30 -0800961 pool = items[2]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800962 self._host_data.append((hostname, model, pool))
xixuan12ce04f2016-03-10 13:16:30 -0800963 self._histories = []
Richard Barnette5de01eb2017-12-15 09:53:42 -0800964 self._histories.append(_FakeHostHistory('echidna', 'bvt', _BROKEN))
965 self._histories.append(_FakeHostHistory('lion', 'bvt', _WORKING))
xixuan12ce04f2016-03-10 13:16:30 -0800966
xixuan12ce04f2016-03-10 13:16:30 -0800967 def _add_idles(self):
968 """Add idle duts from `_IDLE_MESSAGE_TEMPLATE`."""
969 idle_histories = [_FakeHostHistory(
Richard Barnette5de01eb2017-12-15 09:53:42 -0800970 model, pool, _UNUSED, hostname)
971 for hostname, model, pool in self._host_data]
xixuan12ce04f2016-03-10 13:16:30 -0800972 self._histories.extend(idle_histories)
973
xixuan12ce04f2016-03-10 13:16:30 -0800974 def _check_header(self, text):
975 """Check whether header in the template `_IDLE_MESSAGE_TEMPLATE` is in
976 passed text."""
977 self.assertIn(self._header, text)
978
xixuan12ce04f2016-03-10 13:16:30 -0800979 def _get_idle_message(self, histories):
980 """Generate idle inventory and obtain its message.
981
982 @param histories: Used to create lab inventory.
983
984 @return the generated idle message.
xixuan12ce04f2016-03-10 13:16:30 -0800985 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800986 inventory = lab_inventory._LabInventory(
987 histories, lab_inventory.MANAGED_POOLS)
xixuan12ce04f2016-03-10 13:16:30 -0800988 message = lab_inventory._generate_idle_inventory_message(
989 inventory).split('\n')
990 return message
991
xixuan12ce04f2016-03-10 13:16:30 -0800992 def test_check_idle_inventory(self):
993 """Test that reporting all the idle DUTs for every pool, sorted by
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700994 lab_inventory.MANAGED_POOLS.
xixuan12ce04f2016-03-10 13:16:30 -0800995 """
996 self._add_idles()
997
998 message = self._get_idle_message(self._histories)
999 self._check_header(message)
1000 self._check_report(message[message.index(self._header) + 1 :])
1001
xixuan12ce04f2016-03-10 13:16:30 -08001002 def test_no_idle_inventory(self):
1003 """Test that reporting no idle DUTs."""
1004 message = self._get_idle_message(self._histories)
1005 self._check_header(message)
1006 self._check_report_no_info(
1007 message[message.index(self._header) + 1 :])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001008
1009
1010class CommandParsingTests(unittest.TestCase):
1011 """Tests for command line argument parsing in `_parse_command()`."""
1012
Richard Barnette7bfcb032018-02-26 11:46:56 -08001013 # At least one of these options must be specified on every command
1014 # line; otherwise, the command line parsing will fail.
Richard Barnette1ca30e62018-04-09 16:45:58 -07001015 _REPORT_OPTIONS = [
1016 '--model-notify=', '--pool-notify=', '--report-untestable'
1017 ]
J. Richard Barnette02e82432015-10-13 16:02:47 -07001018
J. Richard Barnette96db3492015-03-27 17:23:52 -07001019 def setUp(self):
1020 dirpath = '/usr/local/fubar'
1021 self._command_path = os.path.join(dirpath,
1022 'site_utils',
1023 'arglebargle')
1024 self._logdir = os.path.join(dirpath, lab_inventory._LOGDIR)
1025
Richard Barnette7bfcb032018-02-26 11:46:56 -08001026 def _parse_arguments(self, argv):
1027 """Test parsing with explictly passed report options."""
1028 full_argv = [self._command_path] + argv
J. Richard Barnette96db3492015-03-27 17:23:52 -07001029 return lab_inventory._parse_command(full_argv)
1030
Richard Barnette7bfcb032018-02-26 11:46:56 -08001031 def _parse_non_report_arguments(self, argv):
1032 """Test parsing for non-report command-line options."""
1033 return self._parse_arguments(argv + self._REPORT_OPTIONS)
1034
Richard Barnette7bfcb032018-02-26 11:46:56 -08001035 def _check_non_report_defaults(self, report_option):
1036 arguments = self._parse_arguments([report_option])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001037 self.assertEqual(arguments.duration,
1038 lab_inventory._DEFAULT_DURATION)
Richard Barnettecf5d8342017-10-24 18:13:11 -07001039 self.assertIsNone(arguments.recommend)
J. Richard Barnette02e82432015-10-13 16:02:47 -07001040 self.assertFalse(arguments.debug)
1041 self.assertEqual(arguments.logdir, self._logdir)
Richard Barnette5de01eb2017-12-15 09:53:42 -08001042 self.assertEqual(arguments.modelnames, [])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001043 return arguments
1044
J. Richard Barnette02e82432015-10-13 16:02:47 -07001045 def test_empty_arguments(self):
Richard Barnette7bfcb032018-02-26 11:46:56 -08001046 """Test that no reports requested is an error."""
1047 arguments = self._parse_arguments([])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001048 self.assertIsNone(arguments)
1049
J. Richard Barnette96db3492015-03-27 17:23:52 -07001050 def test_argument_defaults(self):
1051 """Test that option defaults match expectations."""
Richard Barnette7bfcb032018-02-26 11:46:56 -08001052 for report in self._REPORT_OPTIONS:
1053 arguments = self._check_non_report_defaults(report)
1054
Richard Barnette7bfcb032018-02-26 11:46:56 -08001055 def test_model_notify_defaults(self):
1056 """Test defaults when `--model-notify` is specified alone."""
1057 arguments = self._parse_arguments(['--model-notify='])
Richard Barnette5de01eb2017-12-15 09:53:42 -08001058 self.assertEqual(arguments.model_notify, [''])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001059 self.assertEqual(arguments.pool_notify, [])
Richard Barnette1ca30e62018-04-09 16:45:58 -07001060 self.assertFalse(arguments.report_untestable)
Richard Barnette7bfcb032018-02-26 11:46:56 -08001061
Richard Barnette7bfcb032018-02-26 11:46:56 -08001062 def test_pool_notify_defaults(self):
1063 """Test defaults when `--pool-notify` is specified alone."""
1064 arguments = self._parse_arguments(['--pool-notify='])
Richard Barnette5de01eb2017-12-15 09:53:42 -08001065 self.assertEqual(arguments.model_notify, [])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001066 self.assertEqual(arguments.pool_notify, [''])
Richard Barnette1ca30e62018-04-09 16:45:58 -07001067 self.assertFalse(arguments.report_untestable)
Richard Barnette7bfcb032018-02-26 11:46:56 -08001068
Richard Barnette1ca30e62018-04-09 16:45:58 -07001069 def test_report_untestable_defaults(self):
1070 """Test defaults when `--report-untestable` is specified alone."""
1071 arguments = self._parse_arguments(['--report-untestable'])
Richard Barnette7bfcb032018-02-26 11:46:56 -08001072 self.assertEqual(arguments.model_notify, [])
1073 self.assertEqual(arguments.pool_notify, [])
Richard Barnette1ca30e62018-04-09 16:45:58 -07001074 self.assertTrue(arguments.report_untestable)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001075
Richard Barnette5de01eb2017-12-15 09:53:42 -08001076 def test_model_arguments(self):
1077 """Test that non-option arguments are returned in `modelnames`."""
1078 modellist = ['aardvark', 'echidna']
Richard Barnette7bfcb032018-02-26 11:46:56 -08001079 arguments = self._parse_non_report_arguments(modellist)
Richard Barnette5de01eb2017-12-15 09:53:42 -08001080 self.assertEqual(arguments.modelnames, modellist)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001081
Richard Barnettecf5d8342017-10-24 18:13:11 -07001082 def test_recommend_option(self):
1083 """Test parsing of the `--recommend` option."""
1084 for opt in ['-r', '--recommend']:
1085 for recommend in ['5', '55']:
Richard Barnette7bfcb032018-02-26 11:46:56 -08001086 arguments = self._parse_non_report_arguments([opt, recommend])
Richard Barnettecf5d8342017-10-24 18:13:11 -07001087 self.assertEqual(arguments.recommend, int(recommend))
1088
J. Richard Barnette02e82432015-10-13 16:02:47 -07001089 def test_debug_option(self):
1090 """Test parsing of the `--debug` option."""
Richard Barnette7bfcb032018-02-26 11:46:56 -08001091 arguments = self._parse_non_report_arguments(['--debug'])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001092 self.assertTrue(arguments.debug)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001093
J. Richard Barnette96db3492015-03-27 17:23:52 -07001094 def test_duration(self):
1095 """Test parsing of the `--duration` option."""
Richard Barnettecf5d8342017-10-24 18:13:11 -07001096 for opt in ['-d', '--duration']:
1097 for duration in ['1', '11']:
Richard Barnette7bfcb032018-02-26 11:46:56 -08001098 arguments = self._parse_non_report_arguments([opt, duration])
Richard Barnettecf5d8342017-10-24 18:13:11 -07001099 self.assertEqual(arguments.duration, int(duration))
J. Richard Barnette96db3492015-03-27 17:23:52 -07001100
J. Richard Barnette96db3492015-03-27 17:23:52 -07001101 def _check_email_option(self, option, getlist):
1102 """Test parsing of e-mail address options.
1103
Richard Barnette5de01eb2017-12-15 09:53:42 -08001104 This is a helper function to test the `--model-notify` and
J. Richard Barnette96db3492015-03-27 17:23:52 -07001105 `--pool-notify` options. It tests the following cases:
1106 * `--option a1` gives the list [a1]
1107 * `--option ' a1 '` gives the list [a1]
1108 * `--option a1 --option a2` gives the list [a1, a2]
1109 * `--option a1,a2` gives the list [a1, a2]
1110 * `--option 'a1, a2'` gives the list [a1, a2]
1111
1112 @param option The option to be tested.
1113 @param getlist A function to return the option's value from
1114 parsed command line arguments.
J. Richard Barnette96db3492015-03-27 17:23:52 -07001115 """
1116 a1 = 'mumble@mumbler.com'
1117 a2 = 'bumble@bumbler.org'
Richard Barnette7bfcb032018-02-26 11:46:56 -08001118 arguments = self._parse_arguments([option, a1])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001119 self.assertEqual(getlist(arguments), [a1])
Richard Barnette7bfcb032018-02-26 11:46:56 -08001120 arguments = self._parse_arguments([option, ' ' + a1 + ' '])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001121 self.assertEqual(getlist(arguments), [a1])
Richard Barnette7bfcb032018-02-26 11:46:56 -08001122 arguments = self._parse_arguments([option, a1, option, a2])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001123 self.assertEqual(getlist(arguments), [a1, a2])
1124 arguments = self._parse_arguments(
Richard Barnette7bfcb032018-02-26 11:46:56 -08001125 [option, ','.join([a1, a2])])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001126 self.assertEqual(getlist(arguments), [a1, a2])
1127 arguments = self._parse_arguments(
Richard Barnette7bfcb032018-02-26 11:46:56 -08001128 [option, ', '.join([a1, a2])])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001129 self.assertEqual(getlist(arguments), [a1, a2])
1130
Richard Barnette5de01eb2017-12-15 09:53:42 -08001131 def test_model_notify(self):
1132 """Test parsing of the `--model-notify` option."""
1133 self._check_email_option('--model-notify',
1134 lambda a: a.model_notify)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001135
J. Richard Barnette96db3492015-03-27 17:23:52 -07001136 def test_pool_notify(self):
1137 """Test parsing of the `--pool-notify` option."""
1138 self._check_email_option('--pool-notify',
1139 lambda a: a.pool_notify)
1140
Richard Barnettecf5d8342017-10-24 18:13:11 -07001141 def test_logdir_option(self):
J. Richard Barnette96db3492015-03-27 17:23:52 -07001142 """Test parsing of the `--logdir` option."""
1143 logdir = '/usr/local/whatsis/logs'
Richard Barnette7bfcb032018-02-26 11:46:56 -08001144 arguments = self._parse_non_report_arguments(['--logdir', logdir])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001145 self.assertEqual(arguments.logdir, logdir)
1146
1147
1148if __name__ == '__main__':
J. Richard Barnettef60a1ee2015-06-02 10:52:37 -07001149 # Some of the functions we test log messages. Prevent those
1150 # messages from showing up in test output.
1151 logging.getLogger().setLevel(logging.CRITICAL)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001152 unittest.main()