blob: 5418f7e4321539de47c22b055713b3ae232b8dd9 [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
34
35 def last_diagnosis(self):
36 """Return the recorded diagnosis."""
Prathmesh Prabhu58aede82017-11-09 13:34:25 -080037 return self.status, None
J. Richard Barnette96db3492015-03-27 17:23:52 -070038
39
J. Richard Barnettef6839282015-06-01 16:00:35 -070040class _FakeHostLocation(object):
41 """Class to mock `HostJobHistory` for location sorting."""
42
43 _HOSTNAME_FORMAT = 'chromeos%d-row%d-rack%d-host%d'
44
45
46 def __init__(self, location):
47 self.hostname = self._HOSTNAME_FORMAT % location
48
49
50 @property
51 def host(self):
52 """Return a fake host object with a hostname."""
53 return self
54
55
J. Richard Barnette96db3492015-03-27 17:23:52 -070056# Status values that may be returned by `HostJobHistory`.
57#
Richard Barnette5de01eb2017-12-15 09:53:42 -080058# These merely rename the corresponding values in `status_history`
59# for convenience.
J. Richard Barnette96db3492015-03-27 17:23:52 -070060
61_WORKING = status_history.WORKING
Richard Barnette5de01eb2017-12-15 09:53:42 -080062_UNUSED = status_history.UNUSED
63_BROKEN = status_history.BROKEN
64_UNKNOWN = status_history.UNKNOWN
J. Richard Barnette96db3492015-03-27 17:23:52 -070065
66
Richard Barnette5de01eb2017-12-15 09:53:42 -080067class HostSetInventoryTestCase(unittest.TestCase):
68 """Unit tests for class `_HostSetInventory`.
J. Richard Barnette96db3492015-03-27 17:23:52 -070069
70 Coverage is quite basic: mostly just enough to make sure every
71 function gets called, and to make sure that the counting knows
72 the difference between 0 and 1.
73
Richard Barnette5de01eb2017-12-15 09:53:42 -080074 The testing also ensures that all known status values that can be
75 returned by `HostJobHistory` are counted as expected.
J. Richard Barnette96db3492015-03-27 17:23:52 -070076 """
77
78 def setUp(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -080079 super(HostSetInventoryTestCase, self).setUp()
80 self.histories = lab_inventory._HostSetInventory()
J. Richard Barnette96db3492015-03-27 17:23:52 -070081
82
83 def _add_host(self, status):
Richard Barnette5de01eb2017-12-15 09:53:42 -080084 fake = _FakeHostHistory('zebra', lab_inventory.SPARE_POOL, status)
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -080085 self.histories.record_host(fake)
J. Richard Barnette96db3492015-03-27 17:23:52 -070086
87
xixuan12ce04f2016-03-10 13:16:30 -080088 def _check_counts(self, working, broken, idle):
J. Richard Barnette96db3492015-03-27 17:23:52 -070089 """Check that pool counts match expectations.
90
Richard Barnette5de01eb2017-12-15 09:53:42 -080091 Asserts that `get_working()`, `get_broken()`, and `get_idle()`
92 return the given expected values. Also assert that
93 `get_total()` is the sum of all counts.
J. Richard Barnette96db3492015-03-27 17:23:52 -070094
95 @param working The expected total of working devices.
96 @param broken The expected total of broken devices.
Richard Barnette5de01eb2017-12-15 09:53:42 -080097 @param idle The expected total of idle devices.
J. Richard Barnette96db3492015-03-27 17:23:52 -070098 """
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -080099 self.assertEqual(self.histories.get_working(), working)
100 self.assertEqual(self.histories.get_broken(), broken)
101 self.assertEqual(self.histories.get_idle(), idle)
102 self.assertEqual(self.histories.get_total(),
xixuan12ce04f2016-03-10 13:16:30 -0800103 working + broken + idle)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700104
105
106 def test_empty(self):
107 """Test counts when there are no DUTs recorded."""
xixuan12ce04f2016-03-10 13:16:30 -0800108 self._check_counts(0, 0, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700109
110
xixuan12ce04f2016-03-10 13:16:30 -0800111 def test_broken(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800112 """Test counting for broken DUTs."""
xixuan12ce04f2016-03-10 13:16:30 -0800113 self._add_host(_BROKEN)
114 self._check_counts(0, 1, 0)
115
116
Richard Barnette5de01eb2017-12-15 09:53:42 -0800117 def test_working(self):
118 """Test counting for working DUTs."""
119 self._add_host(_WORKING)
120 self._check_counts(1, 0, 0)
121
122
xixuan12ce04f2016-03-10 13:16:30 -0800123 def test_idle(self):
124 """Testing counting for idle status values."""
125 self._add_host(_UNUSED)
126 self._check_counts(0, 0, 1)
127 self._add_host(_UNKNOWN)
128 self._check_counts(0, 0, 2)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700129
130
131 def test_working_then_broken(self):
132 """Test counts after adding a working and then a broken DUT."""
133 self._add_host(_WORKING)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700134 self._add_host(_BROKEN)
xixuan12ce04f2016-03-10 13:16:30 -0800135 self._check_counts(1, 1, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700136
137
138 def test_broken_then_working(self):
139 """Test counts after adding a broken and then a working DUT."""
140 self._add_host(_BROKEN)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700141 self._add_host(_WORKING)
xixuan12ce04f2016-03-10 13:16:30 -0800142 self._check_counts(1, 1, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700143
144
Richard Barnette5de01eb2017-12-15 09:53:42 -0800145class PoolSetInventoryTestCase(unittest.TestCase):
146 """Unit tests for class `_PoolSetInventory`.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700147
Richard Barnette5de01eb2017-12-15 09:53:42 -0800148 Coverage is quite basic: just enough to make sure every function
149 gets called, and to make sure that the counting knows the difference
150 between 0 and 1.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700151
Richard Barnette5de01eb2017-12-15 09:53:42 -0800152 The tests make sure that both individual pool counts and totals are
153 counted correctly.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700154 """
155
Richard Barnette5de01eb2017-12-15 09:53:42 -0800156 _POOL_SET = ['humpty', 'dumpty']
157
J. Richard Barnette96db3492015-03-27 17:23:52 -0700158 def setUp(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800159 super(PoolSetInventoryTestCase, self).setUp()
160 self._pool_histories = lab_inventory._PoolSetInventory(self._POOL_SET)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700161
162
163 def _add_host(self, pool, status):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800164 fake = _FakeHostHistory('zebra', pool, status)
165 self._pool_histories.record_host(fake)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700166
167
168 def _check_all_counts(self, working, broken):
169 """Check that total counts for all pools match expectations.
170
171 Checks that `get_working()` and `get_broken()` return the
172 given expected values when called without a pool specified.
173 Also check that `get_total()` is the sum of working and
174 broken devices.
175
176 Additionally, call the various functions for all the pools
177 individually, and confirm that the totals across pools match
178 the given expectations.
179
180 @param working The expected total of working devices.
181 @param broken The expected total of broken devices.
182
183 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800184 self.assertEqual(self._pool_histories.get_working(), working)
185 self.assertEqual(self._pool_histories.get_broken(), broken)
186 self.assertEqual(self._pool_histories.get_total(),
J. Richard Barnette96db3492015-03-27 17:23:52 -0700187 working + broken)
188 count_working = 0
189 count_broken = 0
190 count_total = 0
Richard Barnette5de01eb2017-12-15 09:53:42 -0800191 for pool in self._POOL_SET:
192 count_working += self._pool_histories.get_working(pool)
193 count_broken += self._pool_histories.get_broken(pool)
194 count_total += self._pool_histories.get_total(pool)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700195 self.assertEqual(count_working, working)
196 self.assertEqual(count_broken, broken)
197 self.assertEqual(count_total, working + broken)
198
199
200 def _check_pool_counts(self, pool, working, broken):
201 """Check that counts for a given pool match expectations.
202
203 Checks that `get_working()` and `get_broken()` return the
204 given expected values for the given pool. Also check that
205 `get_total()` is the sum of working and broken devices.
206
207 @param pool The pool to be checked.
208 @param working The expected total of working devices.
209 @param broken The expected total of broken devices.
210
211 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800212 self.assertEqual(self._pool_histories.get_working(pool),
J. Richard Barnette96db3492015-03-27 17:23:52 -0700213 working)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800214 self.assertEqual(self._pool_histories.get_broken(pool),
J. Richard Barnette96db3492015-03-27 17:23:52 -0700215 broken)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800216 self.assertEqual(self._pool_histories.get_total(pool),
J. Richard Barnette96db3492015-03-27 17:23:52 -0700217 working + broken)
218
219
220 def test_empty(self):
221 """Test counts when there are no DUTs recorded."""
222 self._check_all_counts(0, 0)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800223 for pool in self._POOL_SET:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700224 self._check_pool_counts(pool, 0, 0)
225
226
227 def test_all_working_then_broken(self):
228 """Test counts after adding a working and then a broken DUT.
229
230 For each pool, add first a working, then a broken 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, _WORKING)
237 working += 1
238 self._check_pool_counts(pool, 1, 0)
239 self._check_all_counts(working, broken)
240 self._add_host(pool, _BROKEN)
241 broken += 1
242 self._check_pool_counts(pool, 1, 1)
243 self._check_all_counts(working, broken)
244
245
246 def test_all_broken_then_working(self):
247 """Test counts after adding a broken and then a working DUT.
248
249 For each pool, add first a broken, then a working DUT. After
250 each DUT is added, check counts to confirm the correct values.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700251 """
252 working = 0
253 broken = 0
Richard Barnette5de01eb2017-12-15 09:53:42 -0800254 for pool in self._POOL_SET:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700255 self._add_host(pool, _BROKEN)
256 broken += 1
257 self._check_pool_counts(pool, 0, 1)
258 self._check_all_counts(working, broken)
259 self._add_host(pool, _WORKING)
260 working += 1
261 self._check_pool_counts(pool, 1, 1)
262 self._check_all_counts(working, broken)
263
264
J. Richard Barnettef6839282015-06-01 16:00:35 -0700265class LocationSortTests(unittest.TestCase):
266 """Unit tests for `_sort_by_location()`."""
267
268 def setUp(self):
269 super(LocationSortTests, self).setUp()
270
271
272 def _check_sorting(self, *locations):
273 """Test sorting a given list of locations.
274
275 The input is an already ordered list of lists of tuples with
276 row, rack, and host numbers. The test converts the tuples
277 to hostnames, preserving the original ordering. Then it
278 flattens and scrambles the input, runs it through
279 `_sort_by_location()`, and asserts that the result matches
280 the original.
281
282 """
283 lab = 0
284 expected = []
285 for tuples in locations:
286 lab += 1
287 expected.append(
288 [_FakeHostLocation((lab,) + t) for t in tuples])
289 scrambled = [e for e in itertools.chain(*expected)]
290 scrambled = [e for e in reversed(scrambled)]
291 actual = lab_inventory._sort_by_location(scrambled)
292 # The ordering of the labs in the output isn't guaranteed,
293 # so we can't compare `expected` and `actual` directly.
294 # Instead, we create a dictionary keyed on the first host in
295 # each lab, and compare the dictionaries.
296 self.assertEqual({l[0]: l for l in expected},
297 {l[0]: l for l in actual})
298
299
300 def test_separate_labs(self):
301 """Test that sorting distinguishes labs."""
302 self._check_sorting([(1, 1, 1)], [(1, 1, 1)], [(1, 1, 1)])
303
304
305 def test_separate_rows(self):
306 """Test for proper sorting when only rows are different."""
307 self._check_sorting([(1, 1, 1), (9, 1, 1), (10, 1, 1)])
308
309
310 def test_separate_racks(self):
311 """Test for proper sorting when only racks are different."""
312 self._check_sorting([(1, 1, 1), (1, 9, 1), (1, 10, 1)])
313
314
315 def test_separate_hosts(self):
316 """Test for proper sorting when only hosts are different."""
317 self._check_sorting([(1, 1, 1), (1, 1, 9), (1, 1, 10)])
318
319
320 def test_diagonal(self):
321 """Test for proper sorting when all parts are different."""
322 self._check_sorting([(1, 1, 2), (1, 2, 1), (2, 1, 1)])
323
324
325class InventoryScoringTests(unittest.TestCase):
326 """Unit tests for `_score_repair_set()`."""
327
328 def setUp(self):
329 super(InventoryScoringTests, self).setUp()
330
331
332 def _make_buffer_counts(self, *counts):
333 """Create a dictionary suitable as `buffer_counts`.
334
Richard Barnette5de01eb2017-12-15 09:53:42 -0800335 @param counts List of tuples with model count data.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700336 """
337 self._buffer_counts = dict(counts)
338
339
340 def _make_history_list(self, repair_counts):
341 """Create a list suitable as `repair_list`.
342
Richard Barnette5de01eb2017-12-15 09:53:42 -0800343 @param repair_counts List of (model, count) tuples.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700344 """
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700345 pool = lab_inventory.SPARE_POOL
J. Richard Barnettef6839282015-06-01 16:00:35 -0700346 histories = []
Richard Barnette5de01eb2017-12-15 09:53:42 -0800347 for model, count in repair_counts:
J. Richard Barnettef6839282015-06-01 16:00:35 -0700348 for i in range(0, count):
349 histories.append(
Richard Barnette5de01eb2017-12-15 09:53:42 -0800350 _FakeHostHistory(model, pool, _BROKEN))
J. Richard Barnettef6839282015-06-01 16:00:35 -0700351 return histories
352
353
354 def _check_better(self, repair_a, repair_b):
355 """Test that repair set A scores better than B.
356
357 Contruct repair sets from `repair_a` and `repair_b`,
358 and score both of them using the pre-existing
359 `self._buffer_counts`. Assert that the score for A is
360 better than the score for B.
361
362 @param repair_a Input data for repair set A
363 @param repair_b Input data for repair set B
J. Richard Barnettef6839282015-06-01 16:00:35 -0700364 """
365 score_a = lab_inventory._score_repair_set(
366 self._buffer_counts,
367 self._make_history_list(repair_a))
368 score_b = lab_inventory._score_repair_set(
369 self._buffer_counts,
370 self._make_history_list(repair_b))
371 self.assertGreater(score_a, score_b)
372
373
374 def _check_equal(self, repair_a, repair_b):
375 """Test that repair set A scores the same as B.
376
377 Contruct repair sets from `repair_a` and `repair_b`,
378 and score both of them using the pre-existing
379 `self._buffer_counts`. Assert that the score for A is
380 equal to the score for B.
381
382 @param repair_a Input data for repair set A
383 @param repair_b Input data for repair set B
J. Richard Barnettef6839282015-06-01 16:00:35 -0700384 """
385 score_a = lab_inventory._score_repair_set(
386 self._buffer_counts,
387 self._make_history_list(repair_a))
388 score_b = lab_inventory._score_repair_set(
389 self._buffer_counts,
390 self._make_history_list(repair_b))
391 self.assertEqual(score_a, score_b)
392
393
Richard Barnette5de01eb2017-12-15 09:53:42 -0800394 def test_improve_worst_model(self):
395 """Test that improving the worst model improves scoring.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700396
Richard Barnette5de01eb2017-12-15 09:53:42 -0800397 Construct a buffer counts dictionary with all models having
J. Richard Barnettef6839282015-06-01 16:00:35 -0700398 different counts. Assert that it is both necessary and
Richard Barnette5de01eb2017-12-15 09:53:42 -0800399 sufficient to improve the count of the worst model in order
J. Richard Barnettef6839282015-06-01 16:00:35 -0700400 to improve the score.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700401 """
402 self._make_buffer_counts(('lion', 0),
403 ('tiger', 1),
404 ('bear', 2))
405 self._check_better([('lion', 1)], [('tiger', 1)])
406 self._check_better([('lion', 1)], [('bear', 1)])
407 self._check_better([('lion', 1)], [('tiger', 2)])
408 self._check_better([('lion', 1)], [('bear', 2)])
409 self._check_equal([('tiger', 1)], [('bear', 1)])
410
411
412 def test_improve_worst_case_count(self):
413 """Test that improving the number of worst cases improves the score.
414
Richard Barnette5de01eb2017-12-15 09:53:42 -0800415 Construct a buffer counts dictionary with all models having
416 the same counts. Assert that improving two models is better
417 than improving one. Assert that improving any one model is
J. Richard Barnettef6839282015-06-01 16:00:35 -0700418 as good as any other.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700419 """
420 self._make_buffer_counts(('lion', 0),
421 ('tiger', 0),
422 ('bear', 0))
423 self._check_better([('lion', 1), ('tiger', 1)], [('bear', 2)])
424 self._check_equal([('lion', 2)], [('tiger', 1)])
425 self._check_equal([('tiger', 1)], [('bear', 1)])
426
427
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800428# Each item is the number of DUTs in that status.
429STATUS_CHOICES = (_WORKING, _BROKEN, _UNUSED)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800430StatusCounts = collections.namedtuple('StatusCounts', ['good', 'bad', 'idle'])
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800431# Each item is a StatusCounts tuple specifying the number of DUTs per status in
432# the that pool.
433CRITICAL_POOL = lab_inventory.CRITICAL_POOLS[0]
434SPARE_POOL = lab_inventory.SPARE_POOL
435POOL_CHOICES = (CRITICAL_POOL, SPARE_POOL)
436PoolStatusCounts = collections.namedtuple('PoolStatusCounts',
437 ['critical', 'spare'])
J. Richard Barnette96db3492015-03-27 17:23:52 -0700438
Richard Barnette5de01eb2017-12-15 09:53:42 -0800439def create_inventory(data):
440 """Create a `_LabInventory` instance for testing.
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800441
Richard Barnette5de01eb2017-12-15 09:53:42 -0800442 This function allows the construction of a complete `_LabInventory`
443 object from a simplified input representation.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700444
445 A single 'critical pool' is arbitrarily chosen for purposes of
446 testing; there's no coverage for testing arbitrary combinations
447 in more than one critical pool.
448
Richard Barnette5de01eb2017-12-15 09:53:42 -0800449 @param data: dict {key: PoolStatusCounts}.
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800450 @returns: lab_inventory._LabInventory object.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700451 """
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800452 histories = []
Richard Barnette5de01eb2017-12-15 09:53:42 -0800453 for model, counts in data.iteritems():
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800454 for p, pool in enumerate(POOL_CHOICES):
455 for s, status in enumerate(STATUS_CHOICES):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800456 fake_host = _FakeHostHistory(model, pool, status)
457 histories.extend([fake_host] * counts[p][s])
458 inventory = lab_inventory._LabInventory(
459 histories, lab_inventory.MANAGED_POOLS)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800460 return inventory
J. Richard Barnette96db3492015-03-27 17:23:52 -0700461
Richard Barnette5de01eb2017-12-15 09:53:42 -0800462
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800463class LabInventoryTests(unittest.TestCase):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700464 """Tests for the basic functions of `_LabInventory`.
465
Richard Barnette5de01eb2017-12-15 09:53:42 -0800466 Contains basic coverage to show that after an inventory is created
467 and DUTs with known status are added, the inventory counts match the
468 counts of the added DUTs.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700469 """
470
Richard Barnette5de01eb2017-12-15 09:53:42 -0800471 _MODEL_LIST = ['lion', 'tiger', 'bear'] # Oh, my!
J. Richard Barnette96db3492015-03-27 17:23:52 -0700472
J. Richard Barnettef6839282015-06-01 16:00:35 -0700473
Richard Barnette5de01eb2017-12-15 09:53:42 -0800474 def _check_inventory_counts(self, inventory, data, msg=None):
475 """Check that all counts in the inventory match `data`.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700476
Richard Barnette5de01eb2017-12-15 09:53:42 -0800477 This asserts that the actual counts returned by the various
478 accessor functions for `inventory` match the values expected for
479 the given `data` that created the inventory.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700480
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800481 @param inventory: _LabInventory object to check.
482 @param data Inventory data to check against. Same type as
483 `create_inventory`.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700484 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800485 self.assertEqual(set(inventory.keys()), set(data.keys()))
486 for model, histories in inventory.iteritems():
487 expected_counts = data[model]
488 actual_counts = PoolStatusCounts(
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800489 StatusCounts(
490 histories.get_working(CRITICAL_POOL),
491 histories.get_broken(CRITICAL_POOL),
492 histories.get_idle(CRITICAL_POOL),
493 ),
494 StatusCounts(
495 histories.get_working(SPARE_POOL),
496 histories.get_broken(SPARE_POOL),
497 histories.get_idle(SPARE_POOL),
498 ),
499 )
Richard Barnette5de01eb2017-12-15 09:53:42 -0800500 self.assertEqual(actual_counts, expected_counts, msg)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800501
502 self.assertEqual(len(histories.get_working_list()),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800503 sum([p.good for p in expected_counts]),
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800504 msg)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800505 self.assertEqual(len(histories.get_broken_list()),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800506 sum([p.bad for p in expected_counts]),
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800507 msg)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800508 self.assertEqual(len(histories.get_idle_list()),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800509 sum([p.idle for p in expected_counts]),
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800510 msg)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700511
512
513 def test_empty(self):
514 """Test counts when there are no DUTs recorded."""
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800515 inventory = create_inventory({})
516 self.assertEqual(inventory.get_num_duts(), 0)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800517 self.assertEqual(inventory.get_boards(), set())
518 self._check_inventory_counts(inventory, {})
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800519 self.assertEqual(inventory.get_num_models(), 0)
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700520
521
Richard Barnette5de01eb2017-12-15 09:53:42 -0800522 def _check_model_count(self, model_count):
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800523 """Parameterized test for testing a specific number of models."""
Richard Barnette5de01eb2017-12-15 09:53:42 -0800524 msg = '[model: %d]' % (model_count,)
525 models = self._MODEL_LIST[:model_count]
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800526 data = {
527 m: PoolStatusCounts(
528 StatusCounts(1, 1, 1),
529 StatusCounts(1, 1, 1),
530 )
531 for m in models
532 }
Richard Barnette5de01eb2017-12-15 09:53:42 -0800533 inventory = create_inventory(data)
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800534 self.assertEqual(inventory.get_num_duts(), 6 * model_count, msg)
535 self.assertEqual(inventory.get_num_models(), model_count, msg)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800536 for pool in [CRITICAL_POOL, SPARE_POOL]:
537 self.assertEqual(set(inventory.get_pool_models(pool)),
538 set(models))
539 self._check_inventory_counts(inventory, data, msg=msg)
540
541
542 def test_model_counts(self):
543 """Test counts for various numbers of models."""
544 self.longMessage = True
545 for model_count in range(0, len(self._MODEL_LIST)):
546 self._check_model_count(model_count)
547
548
549 def _check_single_dut_counts(self, critical, spare):
550 """Parmeterized test for single dut counts."""
551 self.longMessage = True
552 counts = PoolStatusCounts(critical, spare)
553 model = self._MODEL_LIST[0]
554 data = {model: counts}
555 msg = '[data: %s]' % (data,)
556 inventory = create_inventory(data)
557 self.assertEqual(inventory.get_num_duts(), 1, msg)
558 self.assertEqual(inventory.get_num_models(), 1, msg)
559 self._check_inventory_counts(inventory, data, msg=msg)
Prathmesh Prabhua5a0e3d2017-11-09 08:53:53 -0800560
J. Richard Barnette96db3492015-03-27 17:23:52 -0700561
562 def test_single_dut_counts(self):
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800563 """Test counts when there is a single DUT per board, and it is good."""
Richard Barnette5de01eb2017-12-15 09:53:42 -0800564 status_100 = StatusCounts(1, 0, 0)
565 status_010 = StatusCounts(0, 1, 0)
566 status_001 = StatusCounts(0, 0, 1)
567 status_null = StatusCounts(0, 0, 0)
568 self._check_single_dut_counts(status_100, status_null)
569 self._check_single_dut_counts(status_010, status_null)
570 self._check_single_dut_counts(status_001, status_null)
571 self._check_single_dut_counts(status_null, status_100)
572 self._check_single_dut_counts(status_null, status_010)
573 self._check_single_dut_counts(status_null, status_001)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700574
575
Richard Barnette5de01eb2017-12-15 09:53:42 -0800576# MODEL_MESSAGE_TEMPLATE -
J. Richard Barnette96db3492015-03-27 17:23:52 -0700577# This is a sample of the output text produced by
Richard Barnette5de01eb2017-12-15 09:53:42 -0800578# _generate_model_inventory_message(). This string is parsed by the
J. Richard Barnette96db3492015-03-27 17:23:52 -0700579# tests below to construct a sample inventory that should produce
580# the output, and then the output is generated and checked against
581# this original sample.
582#
583# Constructing inventories from parsed sample text serves two
584# related purposes:
585# - It provides a way to see what the output should look like
586# without having to run the script.
587# - It helps make sure that a human being will actually look at
588# the output to see that it's basically readable.
589# This should also help prevent test bugs caused by writing tests
590# that simply parrot the original output generation code.
591
Richard Barnette5de01eb2017-12-15 09:53:42 -0800592_MODEL_MESSAGE_TEMPLATE = '''
593Model Avail Bad Idle Good Spare Total
xixuan12ce04f2016-03-10 13:16:30 -0800594lion -1 13 2 11 12 26
595tiger -1 5 2 9 4 16
596bear 0 5 2 10 5 17
597platypus 4 2 2 20 6 24
598aardvark 7 2 2 6 9 10
J. Richard Barnette96db3492015-03-27 17:23:52 -0700599'''
600
601
Richard Barnette5de01eb2017-12-15 09:53:42 -0800602class PoolSetInventoryTests(unittest.TestCase):
603 """Tests for `_generate_model_inventory_message()`.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700604
605 The tests create various test inventories designed to match the
Richard Barnette5de01eb2017-12-15 09:53:42 -0800606 counts in `_MODEL_MESSAGE_TEMPLATE`, and asserts that the
J. Richard Barnette96db3492015-03-27 17:23:52 -0700607 generated message text matches the original message text.
608
609 Message text is represented as a list of strings, split on the
610 `'\n'` separator.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700611 """
612
613 def setUp(self):
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800614 self.maxDiff = None
Richard Barnette5de01eb2017-12-15 09:53:42 -0800615 lines = [x.strip() for x in _MODEL_MESSAGE_TEMPLATE.split('\n') if
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800616 x.strip()]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800617 self._header, self._model_lines = lines[0], lines[1:]
618 self._model_data = []
619 for l in self._model_lines:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700620 items = l.split()
Richard Barnette5de01eb2017-12-15 09:53:42 -0800621 model = items[0]
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800622 bad, idle, good, spare = [int(x) for x in items[2:-1]]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800623 self._model_data.append((model, (good, bad, idle, spare)))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700624
625
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800626
J. Richard Barnette96db3492015-03-27 17:23:52 -0700627 def _make_minimum_spares(self, counts):
628 """Create a counts tuple with as few spare DUTs as possible."""
xixuan12ce04f2016-03-10 13:16:30 -0800629 good, bad, idle, spares = counts
630 if spares > bad + idle:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800631 return PoolStatusCounts(
632 StatusCounts(good + bad +idle - spares, 0, 0),
633 StatusCounts(spares - bad - idle, bad, idle),
634 )
xixuan12ce04f2016-03-10 13:16:30 -0800635 elif spares < bad:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800636 return PoolStatusCounts(
637 StatusCounts(good, bad - spares, idle),
638 StatusCounts(0, spares, 0),
639 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700640 else:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800641 return PoolStatusCounts(
642 StatusCounts(good, 0, idle + bad - spares),
643 StatusCounts(0, bad, spares - bad),
644 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700645
646
647 def _make_maximum_spares(self, counts):
648 """Create a counts tuple with as many spare DUTs as possible."""
xixuan12ce04f2016-03-10 13:16:30 -0800649 good, bad, idle, spares = counts
J. Richard Barnette96db3492015-03-27 17:23:52 -0700650 if good > spares:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800651 return PoolStatusCounts(
652 StatusCounts(good - spares, bad, idle),
653 StatusCounts(spares, 0, 0),
654 )
xixuan12ce04f2016-03-10 13:16:30 -0800655 elif good + bad > spares:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800656 return PoolStatusCounts(
657 StatusCounts(0, good + bad - spares, idle),
658 StatusCounts(good, spares - good, 0),
659 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700660 else:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800661 return PoolStatusCounts(
662 StatusCounts(0, 0, good + bad + idle - spares),
663 StatusCounts(good, bad, spares - good - bad),
664 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700665
666
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800667 def _check_message(self, message):
668 """Checks that message approximately matches expected string."""
669 message = [x.strip() for x in message.split('\n') if x.strip()]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700670 self.assertIn(self._header, message)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800671 body = message[message.index(self._header) + 1:]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800672 self.assertEqual(body, self._model_lines)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700673
674
675 def test_minimum_spares(self):
676 """Test message generation when the spares pool is low."""
677 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800678 model: self._make_minimum_spares(counts)
679 for model, counts in self._model_data
J. Richard Barnette96db3492015-03-27 17:23:52 -0700680 }
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 Barnette96db3492015-03-27 17:23:52 -0700684
685 def test_maximum_spares(self):
686 """Test message generation when the critical pool is low."""
687 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800688 model: self._make_maximum_spares(counts)
689 for model, counts in self._model_data
J. Richard Barnette96db3492015-03-27 17:23:52 -0700690 }
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800691 inventory = create_inventory(data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800692 message = lab_inventory._generate_model_inventory_message(inventory)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800693 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700694
695
696 def test_ignore_no_spares(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800697 """Test that messages ignore models with no spare pool."""
J. Richard Barnette96db3492015-03-27 17:23:52 -0700698 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800699 model: self._make_maximum_spares(counts)
700 for model, counts in self._model_data
J. Richard Barnette96db3492015-03-27 17:23:52 -0700701 }
xixuan12ce04f2016-03-10 13:16:30 -0800702 data['elephant'] = ((5, 4, 0), (0, 0, 0))
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800703 inventory = create_inventory(data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800704 message = lab_inventory._generate_model_inventory_message(inventory)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800705 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700706
707
708 def test_ignore_no_critical(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800709 """Test that messages ignore models with no critical pools."""
J. Richard Barnette96db3492015-03-27 17:23:52 -0700710 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800711 model: self._make_maximum_spares(counts)
712 for model, counts in self._model_data
J. Richard Barnette96db3492015-03-27 17:23:52 -0700713 }
xixuan12ce04f2016-03-10 13:16:30 -0800714 data['elephant'] = ((0, 0, 0), (1, 5, 1))
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800715 inventory = create_inventory(data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800716 message = lab_inventory._generate_model_inventory_message(inventory)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800717 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700718
719
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800720 def test_ignore_no_bad(self):
Richard Barnette5de01eb2017-12-15 09:53:42 -0800721 """Test that messages ignore models with no bad DUTs."""
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800722 data = {
Richard Barnette5de01eb2017-12-15 09:53:42 -0800723 model: self._make_maximum_spares(counts)
724 for model, counts in self._model_data
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800725 }
xixuan12ce04f2016-03-10 13:16:30 -0800726 data['elephant'] = ((5, 0, 1), (5, 0, 1))
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800727 inventory = create_inventory(data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800728 message = lab_inventory._generate_model_inventory_message(inventory)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800729 self._check_message(message)
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800730
731
xixuan12ce04f2016-03-10 13:16:30 -0800732class _PoolInventoryTestBase(unittest.TestCase):
733 """Parent class for tests relating to generating pool inventory messages.
734
735 Func `setUp` in the class parses a given |message_template| to obtain
736 header and body.
737 """
738 def _read_template(self, message_template):
739 """Read message template for PoolInventoryTest and IdleInventoryTest.
740
741 @param message_template: the input template to be parsed into: header
742 and content (report_lines).
743
744 """
745 message_lines = message_template.split('\n')
746 self._header = message_lines[1]
747 self._report_lines = message_lines[2:-1]
748
749
750 def _check_report_no_info(self, text):
751 """Test a message body containing no reported info.
752
753 The input `text` was created from a query to an inventory, which has
754 no objects meet the query and leads to an `empty` return. Assert that
755 the text consists of a single line starting with '(' and ending with ')'.
756
757 @param text: Message body text to be tested.
758
759 """
760 self.assertTrue(len(text) == 1 and
761 text[0][0] == '(' and
762 text[0][-1] == ')')
763
764
765 def _check_report(self, text):
766 """Test a message against the passed |expected_content|.
767
768 @param text: Message body text to be tested.
769 @param expected_content: The ground-truth content to be compared with.
770
771 """
772 self.assertEqual(text, self._report_lines)
773
774
J. Richard Barnette96db3492015-03-27 17:23:52 -0700775# _POOL_MESSAGE_TEMPLATE -
776# This is a sample of the output text produced by
777# _generate_pool_inventory_message(). This string is parsed by the
778# tests below to construct a sample inventory that should produce
779# the output, and then the output is generated and checked against
780# this original sample.
781#
782# See the comments on _BOARD_MESSAGE_TEMPLATE above for the
783# rationale on using sample text in this way.
784
785_POOL_MESSAGE_TEMPLATE = '''
Richard Barnette5de01eb2017-12-15 09:53:42 -0800786Model Bad Idle Good Total
xixuan12ce04f2016-03-10 13:16:30 -0800787lion 5 2 6 13
788tiger 4 1 5 10
789bear 3 0 7 10
790aardvark 2 0 0 2
791platypus 1 1 1 3
J. Richard Barnette96db3492015-03-27 17:23:52 -0700792'''
793
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700794_POOL_ADMIN_URL = 'http://go/cros-manage-duts'
795
796
xixuan12ce04f2016-03-10 13:16:30 -0800797class PoolInventoryTests(_PoolInventoryTestBase):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700798 """Tests for `_generate_pool_inventory_message()`.
799
800 The tests create various test inventories designed to match the
801 counts in `_POOL_MESSAGE_TEMPLATE`, and assert that the
802 generated message text matches the format established in the
803 original message text.
804
805 The output message text is parsed against the following grammar:
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700806 <message> -> <intro> <pool> { "blank line" <pool> }
807 <intro> ->
808 Instructions to depty mentioning the admin page URL
809 A blank line
J. Richard Barnette96db3492015-03-27 17:23:52 -0700810 <pool> ->
811 <description>
812 <header line>
813 <message body>
814 <description> ->
815 Any number of lines describing one pool
816 <header line> ->
817 The header line from `_POOL_MESSAGE_TEMPLATE`
818 <message body> ->
819 Any number of non-blank lines
820
821 After parsing messages into the parts described above, various
822 assertions are tested against the parsed output, including
823 that the message body matches the body from
824 `_POOL_MESSAGE_TEMPLATE`.
825
826 Parse message text is represented as a list of strings, split on
827 the `'\n'` separator.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700828 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800829
J. Richard Barnette96db3492015-03-27 17:23:52 -0700830 def setUp(self):
xixuan12ce04f2016-03-10 13:16:30 -0800831 super(PoolInventoryTests, self)._read_template(_POOL_MESSAGE_TEMPLATE)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800832 self._model_data = []
xixuan12ce04f2016-03-10 13:16:30 -0800833 for l in self._report_lines:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700834 items = l.split()
Richard Barnette5de01eb2017-12-15 09:53:42 -0800835 model = items[0]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700836 bad = int(items[1])
xixuan12ce04f2016-03-10 13:16:30 -0800837 idle = int(items[2])
838 good = int(items[3])
Richard Barnette5de01eb2017-12-15 09:53:42 -0800839 self._model_data.append((model, (good, bad, idle)))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700840
841
Richard Barnette5de01eb2017-12-15 09:53:42 -0800842 def _create_histories(self, pools, model_data):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700843 """Return a list suitable to create a `_LabInventory` object.
844
845 Creates a list of `_FakeHostHistory` objects that can be
846 used to create a lab inventory. `pools` is a list of strings
Richard Barnette5de01eb2017-12-15 09:53:42 -0800847 naming pools, and `model_data` is a list of tuples of the
J. Richard Barnette96db3492015-03-27 17:23:52 -0700848 form
Richard Barnette5de01eb2017-12-15 09:53:42 -0800849 `(model, (goodcount, badcount))`
J. Richard Barnette96db3492015-03-27 17:23:52 -0700850 where
Richard Barnette5de01eb2017-12-15 09:53:42 -0800851 `model` is a model name.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700852 `goodcount` is the number of working DUTs in the pool.
853 `badcount` is the number of broken DUTs in the pool.
854
855 @param pools List of pools for which to create
856 histories.
Richard Barnette5de01eb2017-12-15 09:53:42 -0800857 @param model_data List of tuples containing models and DUT
J. Richard Barnette96db3492015-03-27 17:23:52 -0700858 counts.
859 @return A list of `_FakeHostHistory` objects that can be
860 used to create a `_LabInventory` object.
861
862 """
863 histories = []
xixuan12ce04f2016-03-10 13:16:30 -0800864 status_choices = (_WORKING, _BROKEN, _UNUSED)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700865 for pool in pools:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800866 for model, counts in model_data:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700867 for status, count in zip(status_choices, counts):
868 for x in range(0, count):
869 histories.append(
Richard Barnette5de01eb2017-12-15 09:53:42 -0800870 _FakeHostHistory(model, pool, status))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700871 return histories
872
873
874 def _parse_pool_summaries(self, histories):
875 """Parse message output according to the grammar above.
876
877 Create a lab inventory from the given `histories`, and
878 generate the pool inventory message. Then parse the message
879 and return a dictionary mapping each pool to the message
880 body parsed after that pool.
881
882 Tests the following assertions:
883 * Each <description> contains a mention of exactly one
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700884 pool in the `CRITICAL_POOLS` list.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700885 * Each pool is mentioned in exactly one <description>.
886 Note that the grammar requires the header to appear once
887 for each pool, so the parsing implicitly asserts that the
888 output contains the header.
889
890 @param histories Input used to create the test
891 `_LabInventory` object.
Richard Barnette5de01eb2017-12-15 09:53:42 -0800892 @return A dictionary mapping model names to the output
893 (a list of lines) for the model.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700894
895 """
Richard Barnette5de01eb2017-12-15 09:53:42 -0800896 inventory = lab_inventory._LabInventory(
897 histories, lab_inventory.MANAGED_POOLS)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700898 message = lab_inventory._generate_pool_inventory_message(
xixuan12ce04f2016-03-10 13:16:30 -0800899 inventory).split('\n')
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700900 poolset = set(lab_inventory.CRITICAL_POOLS)
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700901 seen_url = False
902 seen_intro = False
J. Richard Barnette96db3492015-03-27 17:23:52 -0700903 description = ''
Richard Barnette5de01eb2017-12-15 09:53:42 -0800904 model_text = {}
J. Richard Barnette96db3492015-03-27 17:23:52 -0700905 current_pool = None
906 for line in message:
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700907 if not seen_url:
908 if _POOL_ADMIN_URL in line:
909 seen_url = True
910 elif not seen_intro:
911 if not line:
912 seen_intro = True
913 elif current_pool is None:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700914 if line == self._header:
915 pools_mentioned = [p for p in poolset
916 if p in description]
917 self.assertEqual(len(pools_mentioned), 1)
918 current_pool = pools_mentioned[0]
919 description = ''
Richard Barnette5de01eb2017-12-15 09:53:42 -0800920 model_text[current_pool] = []
J. Richard Barnette96db3492015-03-27 17:23:52 -0700921 poolset.remove(current_pool)
922 else:
923 description += line
924 else:
925 if line:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800926 model_text[current_pool].append(line)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700927 else:
928 current_pool = None
929 self.assertEqual(len(poolset), 0)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800930 return model_text
J. Richard Barnette96db3492015-03-27 17:23:52 -0700931
932
J. Richard Barnette96db3492015-03-27 17:23:52 -0700933 def test_no_shortages(self):
934 """Test correct output when no pools have shortages."""
Richard Barnette5de01eb2017-12-15 09:53:42 -0800935 model_text = self._parse_pool_summaries([])
936 for text in model_text.values():
xixuan12ce04f2016-03-10 13:16:30 -0800937 self._check_report_no_info(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700938
939
940 def test_one_pool_shortage(self):
941 """Test correct output when exactly one pool has a shortage."""
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700942 for pool in lab_inventory.CRITICAL_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700943 histories = self._create_histories((pool,),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800944 self._model_data)
945 model_text = self._parse_pool_summaries(histories)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700946 for checkpool in lab_inventory.CRITICAL_POOLS:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800947 text = model_text[checkpool]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700948 if checkpool == pool:
xixuan12ce04f2016-03-10 13:16:30 -0800949 self._check_report(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700950 else:
xixuan12ce04f2016-03-10 13:16:30 -0800951 self._check_report_no_info(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700952
953
954 def test_all_pool_shortages(self):
955 """Test correct output when all pools have a shortage."""
956 histories = []
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700957 for pool in lab_inventory.CRITICAL_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700958 histories.extend(
959 self._create_histories((pool,),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800960 self._model_data))
961 model_text = self._parse_pool_summaries(histories)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700962 for pool in lab_inventory.CRITICAL_POOLS:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800963 self._check_report(model_text[pool])
J. Richard Barnette96db3492015-03-27 17:23:52 -0700964
965
Richard Barnette5de01eb2017-12-15 09:53:42 -0800966 def test_full_model_ignored(self):
967 """Test that models at full strength are not reported."""
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700968 pool = lab_inventory.CRITICAL_POOLS[0]
Richard Barnette5de01eb2017-12-15 09:53:42 -0800969 full_model = [('echidna', (5, 0, 0))]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700970 histories = self._create_histories((pool,),
Richard Barnette5de01eb2017-12-15 09:53:42 -0800971 full_model)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700972 text = self._parse_pool_summaries(histories)[pool]
xixuan12ce04f2016-03-10 13:16:30 -0800973 self._check_report_no_info(text)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800974 model_data = self._model_data + full_model
975 histories = self._create_histories((pool,), model_data)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700976 text = self._parse_pool_summaries(histories)[pool]
xixuan12ce04f2016-03-10 13:16:30 -0800977 self._check_report(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700978
979
980 def test_spare_pool_ignored(self):
981 """Test that reporting ignores the spare pool inventory."""
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700982 spare_pool = lab_inventory.SPARE_POOL
Richard Barnette5de01eb2017-12-15 09:53:42 -0800983 spare_data = self._model_data + [('echidna', (0, 5, 0))]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700984 histories = self._create_histories((spare_pool,),
985 spare_data)
Richard Barnette5de01eb2017-12-15 09:53:42 -0800986 model_text = self._parse_pool_summaries(histories)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700987 for pool in lab_inventory.CRITICAL_POOLS:
Richard Barnette5de01eb2017-12-15 09:53:42 -0800988 self._check_report_no_info(model_text[pool])
xixuan12ce04f2016-03-10 13:16:30 -0800989
990
991_IDLE_MESSAGE_TEMPLATE = '''
Richard Barnette5de01eb2017-12-15 09:53:42 -0800992Hostname Model Pool
xixuan12ce04f2016-03-10 13:16:30 -0800993chromeos4-row12-rack4-host7 tiger bvt
994chromeos1-row3-rack1-host2 lion bvt
995chromeos3-row2-rack2-host5 lion cq
996chromeos2-row7-rack3-host11 platypus suites
997'''
998
999
1000class IdleInventoryTests(_PoolInventoryTestBase):
1001 """Tests for `_generate_idle_inventory_message()`.
1002
1003 The tests create idle duts that match the counts and pool in
1004 `_IDLE_MESSAGE_TEMPLATE`. In test, it asserts that the generated
1005 idle message text matches the format established in
1006 `_IDLE_MESSAGE_TEMPLATE`.
1007
1008 Parse message text is represented as a list of strings, split on
1009 the `'\n'` separator.
1010
1011 """
1012
1013 def setUp(self):
1014 super(IdleInventoryTests, self)._read_template(_IDLE_MESSAGE_TEMPLATE)
1015 self._host_data = []
1016 for h in self._report_lines:
1017 items = h.split()
1018 hostname = items[0]
Richard Barnette5de01eb2017-12-15 09:53:42 -08001019 model = items[1]
xixuan12ce04f2016-03-10 13:16:30 -08001020 pool = items[2]
Richard Barnette5de01eb2017-12-15 09:53:42 -08001021 self._host_data.append((hostname, model, pool))
xixuan12ce04f2016-03-10 13:16:30 -08001022 self._histories = []
Richard Barnette5de01eb2017-12-15 09:53:42 -08001023 self._histories.append(_FakeHostHistory('echidna', 'bvt', _BROKEN))
1024 self._histories.append(_FakeHostHistory('lion', 'bvt', _WORKING))
xixuan12ce04f2016-03-10 13:16:30 -08001025
1026
1027 def _add_idles(self):
1028 """Add idle duts from `_IDLE_MESSAGE_TEMPLATE`."""
1029 idle_histories = [_FakeHostHistory(
Richard Barnette5de01eb2017-12-15 09:53:42 -08001030 model, pool, _UNUSED, hostname)
1031 for hostname, model, pool in self._host_data]
xixuan12ce04f2016-03-10 13:16:30 -08001032 self._histories.extend(idle_histories)
1033
1034
1035 def _check_header(self, text):
1036 """Check whether header in the template `_IDLE_MESSAGE_TEMPLATE` is in
1037 passed text."""
1038 self.assertIn(self._header, text)
1039
1040
1041 def _get_idle_message(self, histories):
1042 """Generate idle inventory and obtain its message.
1043
1044 @param histories: Used to create lab inventory.
1045
1046 @return the generated idle message.
1047
1048 """
Richard Barnette5de01eb2017-12-15 09:53:42 -08001049 inventory = lab_inventory._LabInventory(
1050 histories, lab_inventory.MANAGED_POOLS)
xixuan12ce04f2016-03-10 13:16:30 -08001051 message = lab_inventory._generate_idle_inventory_message(
1052 inventory).split('\n')
1053 return message
1054
1055
1056 def test_check_idle_inventory(self):
1057 """Test that reporting all the idle DUTs for every pool, sorted by
Kevin Chengcf0ad2b2016-04-19 14:51:39 -07001058 lab_inventory.MANAGED_POOLS.
xixuan12ce04f2016-03-10 13:16:30 -08001059 """
1060 self._add_idles()
1061
1062 message = self._get_idle_message(self._histories)
1063 self._check_header(message)
1064 self._check_report(message[message.index(self._header) + 1 :])
1065
1066
1067 def test_no_idle_inventory(self):
1068 """Test that reporting no idle DUTs."""
1069 message = self._get_idle_message(self._histories)
1070 self._check_header(message)
1071 self._check_report_no_info(
1072 message[message.index(self._header) + 1 :])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001073
1074
1075class CommandParsingTests(unittest.TestCase):
1076 """Tests for command line argument parsing in `_parse_command()`."""
1077
Richard Barnette7bfcb032018-02-26 11:46:56 -08001078 # At least one of these options must be specified on every command
1079 # line; otherwise, the command line parsing will fail.
Richard Barnette1ca30e62018-04-09 16:45:58 -07001080 _REPORT_OPTIONS = [
1081 '--model-notify=', '--pool-notify=', '--report-untestable'
1082 ]
J. Richard Barnette02e82432015-10-13 16:02:47 -07001083
J. Richard Barnette96db3492015-03-27 17:23:52 -07001084 def setUp(self):
1085 dirpath = '/usr/local/fubar'
1086 self._command_path = os.path.join(dirpath,
1087 'site_utils',
1088 'arglebargle')
1089 self._logdir = os.path.join(dirpath, lab_inventory._LOGDIR)
1090
1091
Richard Barnette7bfcb032018-02-26 11:46:56 -08001092 def _parse_arguments(self, argv):
1093 """Test parsing with explictly passed report options."""
1094 full_argv = [self._command_path] + argv
J. Richard Barnette96db3492015-03-27 17:23:52 -07001095 return lab_inventory._parse_command(full_argv)
1096
1097
Richard Barnette7bfcb032018-02-26 11:46:56 -08001098 def _parse_non_report_arguments(self, argv):
1099 """Test parsing for non-report command-line options."""
1100 return self._parse_arguments(argv + self._REPORT_OPTIONS)
1101
1102
1103 def _check_non_report_defaults(self, report_option):
1104 arguments = self._parse_arguments([report_option])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001105 self.assertEqual(arguments.duration,
1106 lab_inventory._DEFAULT_DURATION)
Richard Barnettecf5d8342017-10-24 18:13:11 -07001107 self.assertIsNone(arguments.recommend)
J. Richard Barnette02e82432015-10-13 16:02:47 -07001108 self.assertFalse(arguments.debug)
1109 self.assertEqual(arguments.logdir, self._logdir)
Richard Barnette5de01eb2017-12-15 09:53:42 -08001110 self.assertEqual(arguments.modelnames, [])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001111 return arguments
1112
1113
1114 def test_empty_arguments(self):
Richard Barnette7bfcb032018-02-26 11:46:56 -08001115 """Test that no reports requested is an error."""
1116 arguments = self._parse_arguments([])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001117 self.assertIsNone(arguments)
1118
1119
J. Richard Barnette96db3492015-03-27 17:23:52 -07001120 def test_argument_defaults(self):
1121 """Test that option defaults match expectations."""
Richard Barnette7bfcb032018-02-26 11:46:56 -08001122 for report in self._REPORT_OPTIONS:
1123 arguments = self._check_non_report_defaults(report)
1124
1125
1126 def test_model_notify_defaults(self):
1127 """Test defaults when `--model-notify` is specified alone."""
1128 arguments = self._parse_arguments(['--model-notify='])
Richard Barnette5de01eb2017-12-15 09:53:42 -08001129 self.assertEqual(arguments.model_notify, [''])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001130 self.assertEqual(arguments.pool_notify, [])
Richard Barnette1ca30e62018-04-09 16:45:58 -07001131 self.assertFalse(arguments.report_untestable)
Richard Barnette7bfcb032018-02-26 11:46:56 -08001132
1133
1134 def test_pool_notify_defaults(self):
1135 """Test defaults when `--pool-notify` is specified alone."""
1136 arguments = self._parse_arguments(['--pool-notify='])
Richard Barnette5de01eb2017-12-15 09:53:42 -08001137 self.assertEqual(arguments.model_notify, [])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001138 self.assertEqual(arguments.pool_notify, [''])
Richard Barnette1ca30e62018-04-09 16:45:58 -07001139 self.assertFalse(arguments.report_untestable)
Richard Barnette7bfcb032018-02-26 11:46:56 -08001140
1141
Richard Barnette1ca30e62018-04-09 16:45:58 -07001142 def test_report_untestable_defaults(self):
1143 """Test defaults when `--report-untestable` is specified alone."""
1144 arguments = self._parse_arguments(['--report-untestable'])
Richard Barnette7bfcb032018-02-26 11:46:56 -08001145 self.assertEqual(arguments.model_notify, [])
1146 self.assertEqual(arguments.pool_notify, [])
Richard Barnette1ca30e62018-04-09 16:45:58 -07001147 self.assertTrue(arguments.report_untestable)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001148
1149
Richard Barnette5de01eb2017-12-15 09:53:42 -08001150 def test_model_arguments(self):
1151 """Test that non-option arguments are returned in `modelnames`."""
1152 modellist = ['aardvark', 'echidna']
Richard Barnette7bfcb032018-02-26 11:46:56 -08001153 arguments = self._parse_non_report_arguments(modellist)
Richard Barnette5de01eb2017-12-15 09:53:42 -08001154 self.assertEqual(arguments.modelnames, modellist)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001155
1156
Richard Barnettecf5d8342017-10-24 18:13:11 -07001157 def test_recommend_option(self):
1158 """Test parsing of the `--recommend` option."""
1159 for opt in ['-r', '--recommend']:
1160 for recommend in ['5', '55']:
Richard Barnette7bfcb032018-02-26 11:46:56 -08001161 arguments = self._parse_non_report_arguments([opt, recommend])
Richard Barnettecf5d8342017-10-24 18:13:11 -07001162 self.assertEqual(arguments.recommend, int(recommend))
1163
1164
J. Richard Barnette02e82432015-10-13 16:02:47 -07001165 def test_debug_option(self):
1166 """Test parsing of the `--debug` option."""
Richard Barnette7bfcb032018-02-26 11:46:56 -08001167 arguments = self._parse_non_report_arguments(['--debug'])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001168 self.assertTrue(arguments.debug)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001169
1170
1171 def test_duration(self):
1172 """Test parsing of the `--duration` option."""
Richard Barnettecf5d8342017-10-24 18:13:11 -07001173 for opt in ['-d', '--duration']:
1174 for duration in ['1', '11']:
Richard Barnette7bfcb032018-02-26 11:46:56 -08001175 arguments = self._parse_non_report_arguments([opt, duration])
Richard Barnettecf5d8342017-10-24 18:13:11 -07001176 self.assertEqual(arguments.duration, int(duration))
J. Richard Barnette96db3492015-03-27 17:23:52 -07001177
1178
1179 def _check_email_option(self, option, getlist):
1180 """Test parsing of e-mail address options.
1181
Richard Barnette5de01eb2017-12-15 09:53:42 -08001182 This is a helper function to test the `--model-notify` and
J. Richard Barnette96db3492015-03-27 17:23:52 -07001183 `--pool-notify` options. It tests the following cases:
1184 * `--option a1` gives the list [a1]
1185 * `--option ' a1 '` gives the list [a1]
1186 * `--option a1 --option a2` gives the list [a1, a2]
1187 * `--option a1,a2` gives the list [a1, a2]
1188 * `--option 'a1, a2'` gives the list [a1, a2]
1189
1190 @param option The option to be tested.
1191 @param getlist A function to return the option's value from
1192 parsed command line arguments.
1193
1194 """
1195 a1 = 'mumble@mumbler.com'
1196 a2 = 'bumble@bumbler.org'
Richard Barnette7bfcb032018-02-26 11:46:56 -08001197 arguments = self._parse_arguments([option, a1])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001198 self.assertEqual(getlist(arguments), [a1])
Richard Barnette7bfcb032018-02-26 11:46:56 -08001199 arguments = self._parse_arguments([option, ' ' + a1 + ' '])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001200 self.assertEqual(getlist(arguments), [a1])
Richard Barnette7bfcb032018-02-26 11:46:56 -08001201 arguments = self._parse_arguments([option, a1, option, a2])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001202 self.assertEqual(getlist(arguments), [a1, a2])
1203 arguments = self._parse_arguments(
Richard Barnette7bfcb032018-02-26 11:46:56 -08001204 [option, ','.join([a1, a2])])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001205 self.assertEqual(getlist(arguments), [a1, a2])
1206 arguments = self._parse_arguments(
Richard Barnette7bfcb032018-02-26 11:46:56 -08001207 [option, ', '.join([a1, a2])])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001208 self.assertEqual(getlist(arguments), [a1, a2])
1209
1210
Richard Barnette5de01eb2017-12-15 09:53:42 -08001211 def test_model_notify(self):
1212 """Test parsing of the `--model-notify` option."""
1213 self._check_email_option('--model-notify',
1214 lambda a: a.model_notify)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001215
1216
1217 def test_pool_notify(self):
1218 """Test parsing of the `--pool-notify` option."""
1219 self._check_email_option('--pool-notify',
1220 lambda a: a.pool_notify)
1221
1222
Richard Barnettecf5d8342017-10-24 18:13:11 -07001223 def test_logdir_option(self):
J. Richard Barnette96db3492015-03-27 17:23:52 -07001224 """Test parsing of the `--logdir` option."""
1225 logdir = '/usr/local/whatsis/logs'
Richard Barnette7bfcb032018-02-26 11:46:56 -08001226 arguments = self._parse_non_report_arguments(['--logdir', logdir])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001227 self.assertEqual(arguments.logdir, logdir)
1228
1229
1230if __name__ == '__main__':
J. Richard Barnettef60a1ee2015-06-02 10:52:37 -07001231 # Some of the functions we test log messages. Prevent those
1232 # messages from showing up in test output.
1233 logging.getLogger().setLevel(logging.CRITICAL)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001234 unittest.main()