blob: ae5b3176302ca1d63f2390e7153ed30e8b34eb80 [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
Prathmesh Prabhu58aede82017-11-09 13:34:25 -080020 def __init__(self, hostname, labels):
xixuan12ce04f2016-03-10 13:16:30 -080021 self.hostname = hostname
Prathmesh Prabhu58aede82017-11-09 13:34:25 -080022 self.labels = labels
xixuan12ce04f2016-03-10 13:16:30 -080023
24
J. Richard Barnette96db3492015-03-27 17:23:52 -070025class _FakeHostHistory(object):
26 """Class to mock `HostJobHistory` for testing."""
27
Prathmesh Prabhu58aede82017-11-09 13:34:25 -080028 def __init__(self, board=None, model=None, pool=None, status=None,
29 hostname=''):
30 self.host_board = board
31 self.host_pool = pool
32 self.status = status
33 self.host = _FakeHost(
34 hostname,
35 labels=[
36 'board:%s' % board,
37 'model:%s' % model,
38 'pool:%s' % pool,
39 ],
40 )
J. Richard Barnette96db3492015-03-27 17:23:52 -070041
42
43 def last_diagnosis(self):
44 """Return the recorded diagnosis."""
Prathmesh Prabhu58aede82017-11-09 13:34:25 -080045 return self.status, None
J. Richard Barnette96db3492015-03-27 17:23:52 -070046
47
J. Richard Barnettef6839282015-06-01 16:00:35 -070048class _FakeHostLocation(object):
49 """Class to mock `HostJobHistory` for location sorting."""
50
51 _HOSTNAME_FORMAT = 'chromeos%d-row%d-rack%d-host%d'
52
53
54 def __init__(self, location):
55 self.hostname = self._HOSTNAME_FORMAT % location
56
57
58 @property
59 def host(self):
60 """Return a fake host object with a hostname."""
61 return self
62
63
J. Richard Barnette96db3492015-03-27 17:23:52 -070064# Status values that may be returned by `HostJobHistory`.
65#
66# _NON_WORKING_STATUS_LIST - The complete list (as of this writing)
67# of status values that the lab_inventory module treats as
68# "broken".
69# _WORKING - A value that counts as "working" for purposes
70# of the lab_inventory module.
71# _BROKEN - A value that counts as "broken" for the lab_inventory
72# module. Since there's more than one valid choice here, we've
73# picked one to stand for all of them.
74
75_NON_WORKING_STATUS_LIST = [
76 status_history.UNUSED,
77 status_history.BROKEN,
78 status_history.UNKNOWN,
79]
80
81_WORKING = status_history.WORKING
xixuan12ce04f2016-03-10 13:16:30 -080082_UNUSED = _NON_WORKING_STATUS_LIST[0]
83_BROKEN = _NON_WORKING_STATUS_LIST[1]
84_UNKNOWN = _NON_WORKING_STATUS_LIST[2]
J. Richard Barnette96db3492015-03-27 17:23:52 -070085
86
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -080087class CachedHostJobHistoriesTestCase(unittest.TestCase):
88 """Unit tests for class `_CachedHostJobHistories`.
J. Richard Barnette96db3492015-03-27 17:23:52 -070089
90 Coverage is quite basic: mostly just enough to make sure every
91 function gets called, and to make sure that the counting knows
92 the difference between 0 and 1.
93
94 The testing also ensures that all known status values that
95 can be returned by `HostJobHistory` are counted as expected.
96
97 """
98
99 def setUp(self):
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -0800100 super(CachedHostJobHistoriesTestCase, self).setUp()
101 self.histories = lab_inventory._CachedHostJobHistories()
J. Richard Barnette96db3492015-03-27 17:23:52 -0700102
103
104 def _add_host(self, status):
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800105 fake = _FakeHostHistory(pool=lab_inventory.SPARE_POOL, status=status)
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -0800106 self.histories.record_host(fake)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700107
108
xixuan12ce04f2016-03-10 13:16:30 -0800109 def _check_counts(self, working, broken, idle):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700110 """Check that pool counts match expectations.
111
112 Checks that `get_working()` and `get_broken()` return the
113 given expected values. Also check that `get_total()` is the
114 sum of working and broken devices.
115
116 @param working The expected total of working devices.
117 @param broken The expected total of broken devices.
118
119 """
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -0800120 self.assertEqual(self.histories.get_working(), working)
121 self.assertEqual(self.histories.get_broken(), broken)
122 self.assertEqual(self.histories.get_idle(), idle)
123 self.assertEqual(self.histories.get_total(),
xixuan12ce04f2016-03-10 13:16:30 -0800124 working + broken + idle)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700125
126
127 def test_empty(self):
128 """Test counts when there are no DUTs recorded."""
xixuan12ce04f2016-03-10 13:16:30 -0800129 self._check_counts(0, 0, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700130
131
xixuan12ce04f2016-03-10 13:16:30 -0800132 def test_broken(self):
133 """Test counting for status: BROKEN."""
134 self._add_host(_BROKEN)
135 self._check_counts(0, 1, 0)
136
137
138 def test_idle(self):
139 """Testing counting for idle status values."""
140 self._add_host(_UNUSED)
141 self._check_counts(0, 0, 1)
142 self._add_host(_UNKNOWN)
143 self._check_counts(0, 0, 2)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700144
145
146 def test_working_then_broken(self):
147 """Test counts after adding a working and then a broken DUT."""
148 self._add_host(_WORKING)
xixuan12ce04f2016-03-10 13:16:30 -0800149 self._check_counts(1, 0, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700150 self._add_host(_BROKEN)
xixuan12ce04f2016-03-10 13:16:30 -0800151 self._check_counts(1, 1, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700152
153
154 def test_broken_then_working(self):
155 """Test counts after adding a broken and then a working DUT."""
156 self._add_host(_BROKEN)
xixuan12ce04f2016-03-10 13:16:30 -0800157 self._check_counts(0, 1, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700158 self._add_host(_WORKING)
xixuan12ce04f2016-03-10 13:16:30 -0800159 self._check_counts(1, 1, 0)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700160
161
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -0800162class ManagedPoolsHostJobHistoriesTestCase(unittest.TestCase):
163 """Unit tests for class `_ManagedPoolsHostJobHistories`.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700164
165 Coverage is quite basic: just enough to make sure every
166 function gets called, and to make sure that the counting
167 knows the difference between 0 and 1.
168
169 The tests make sure that both individual pool counts and
170 totals are counted correctly.
171
172 """
173
174 def setUp(self):
Prathmesh Prabhu0ecbf322017-11-08 17:04:24 -0800175 super(ManagedPoolsHostJobHistoriesTestCase, self).setUp()
176 self._board_counts = lab_inventory._ManagedPoolsHostJobHistories()
J. Richard Barnette96db3492015-03-27 17:23:52 -0700177
178
179 def _add_host(self, pool, status):
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800180 fake = _FakeHostHistory(pool=pool, status=status)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700181 self._board_counts.record_host(fake)
182
183
184 def _check_all_counts(self, working, broken):
185 """Check that total counts for all pools match expectations.
186
187 Checks that `get_working()` and `get_broken()` return the
188 given expected values when called without a pool specified.
189 Also check that `get_total()` is the sum of working and
190 broken devices.
191
192 Additionally, call the various functions for all the pools
193 individually, and confirm that the totals across pools match
194 the given expectations.
195
196 @param working The expected total of working devices.
197 @param broken The expected total of broken devices.
198
199 """
200 self.assertEqual(self._board_counts.get_working(), working)
201 self.assertEqual(self._board_counts.get_broken(), broken)
202 self.assertEqual(self._board_counts.get_total(),
203 working + broken)
204 count_working = 0
205 count_broken = 0
206 count_total = 0
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700207 for pool in lab_inventory.MANAGED_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700208 count_working += self._board_counts.get_working(pool)
209 count_broken += self._board_counts.get_broken(pool)
210 count_total += self._board_counts.get_total(pool)
211 self.assertEqual(count_working, working)
212 self.assertEqual(count_broken, broken)
213 self.assertEqual(count_total, working + broken)
214
215
216 def _check_pool_counts(self, pool, working, broken):
217 """Check that counts for a given pool match expectations.
218
219 Checks that `get_working()` and `get_broken()` return the
220 given expected values for the given pool. Also check that
221 `get_total()` is the sum of working and broken devices.
222
223 @param pool The pool to be checked.
224 @param working The expected total of working devices.
225 @param broken The expected total of broken devices.
226
227 """
228 self.assertEqual(self._board_counts.get_working(pool),
229 working)
230 self.assertEqual(self._board_counts.get_broken(pool),
231 broken)
232 self.assertEqual(self._board_counts.get_total(pool),
233 working + broken)
234
235
236 def test_empty(self):
237 """Test counts when there are no DUTs recorded."""
238 self._check_all_counts(0, 0)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700239 for pool in lab_inventory.MANAGED_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700240 self._check_pool_counts(pool, 0, 0)
241
242
243 def test_all_working_then_broken(self):
244 """Test counts after adding a working and then a broken DUT.
245
246 For each pool, add first a working, then a broken DUT. After
247 each DUT is added, check counts to confirm the correct values.
248
249 """
250 working = 0
251 broken = 0
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700252 for pool in lab_inventory.MANAGED_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700253 self._add_host(pool, _WORKING)
254 working += 1
255 self._check_pool_counts(pool, 1, 0)
256 self._check_all_counts(working, broken)
257 self._add_host(pool, _BROKEN)
258 broken += 1
259 self._check_pool_counts(pool, 1, 1)
260 self._check_all_counts(working, broken)
261
262
263 def test_all_broken_then_working(self):
264 """Test counts after adding a broken and then a working DUT.
265
266 For each pool, add first a broken, then a working DUT. After
267 each DUT is added, check counts to confirm the correct values.
268
269 """
270 working = 0
271 broken = 0
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700272 for pool in lab_inventory.MANAGED_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700273 self._add_host(pool, _BROKEN)
274 broken += 1
275 self._check_pool_counts(pool, 0, 1)
276 self._check_all_counts(working, broken)
277 self._add_host(pool, _WORKING)
278 working += 1
279 self._check_pool_counts(pool, 1, 1)
280 self._check_all_counts(working, broken)
281
282
J. Richard Barnettef6839282015-06-01 16:00:35 -0700283class LocationSortTests(unittest.TestCase):
284 """Unit tests for `_sort_by_location()`."""
285
286 def setUp(self):
287 super(LocationSortTests, self).setUp()
288
289
290 def _check_sorting(self, *locations):
291 """Test sorting a given list of locations.
292
293 The input is an already ordered list of lists of tuples with
294 row, rack, and host numbers. The test converts the tuples
295 to hostnames, preserving the original ordering. Then it
296 flattens and scrambles the input, runs it through
297 `_sort_by_location()`, and asserts that the result matches
298 the original.
299
300 """
301 lab = 0
302 expected = []
303 for tuples in locations:
304 lab += 1
305 expected.append(
306 [_FakeHostLocation((lab,) + t) for t in tuples])
307 scrambled = [e for e in itertools.chain(*expected)]
308 scrambled = [e for e in reversed(scrambled)]
309 actual = lab_inventory._sort_by_location(scrambled)
310 # The ordering of the labs in the output isn't guaranteed,
311 # so we can't compare `expected` and `actual` directly.
312 # Instead, we create a dictionary keyed on the first host in
313 # each lab, and compare the dictionaries.
314 self.assertEqual({l[0]: l for l in expected},
315 {l[0]: l for l in actual})
316
317
318 def test_separate_labs(self):
319 """Test that sorting distinguishes labs."""
320 self._check_sorting([(1, 1, 1)], [(1, 1, 1)], [(1, 1, 1)])
321
322
323 def test_separate_rows(self):
324 """Test for proper sorting when only rows are different."""
325 self._check_sorting([(1, 1, 1), (9, 1, 1), (10, 1, 1)])
326
327
328 def test_separate_racks(self):
329 """Test for proper sorting when only racks are different."""
330 self._check_sorting([(1, 1, 1), (1, 9, 1), (1, 10, 1)])
331
332
333 def test_separate_hosts(self):
334 """Test for proper sorting when only hosts are different."""
335 self._check_sorting([(1, 1, 1), (1, 1, 9), (1, 1, 10)])
336
337
338 def test_diagonal(self):
339 """Test for proper sorting when all parts are different."""
340 self._check_sorting([(1, 1, 2), (1, 2, 1), (2, 1, 1)])
341
342
343class InventoryScoringTests(unittest.TestCase):
344 """Unit tests for `_score_repair_set()`."""
345
346 def setUp(self):
347 super(InventoryScoringTests, self).setUp()
348
349
350 def _make_buffer_counts(self, *counts):
351 """Create a dictionary suitable as `buffer_counts`.
352
353 @param counts List of tuples with board count data.
354
355 """
356 self._buffer_counts = dict(counts)
357
358
359 def _make_history_list(self, repair_counts):
360 """Create a list suitable as `repair_list`.
361
362 @param repair_counts List of (board, count) tuples.
363
364 """
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700365 pool = lab_inventory.SPARE_POOL
J. Richard Barnettef6839282015-06-01 16:00:35 -0700366 histories = []
367 for board, count in repair_counts:
368 for i in range(0, count):
369 histories.append(
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800370 _FakeHostHistory(board=board, pool=pool, status=_BROKEN))
J. Richard Barnettef6839282015-06-01 16:00:35 -0700371 return histories
372
373
374 def _check_better(self, repair_a, repair_b):
375 """Test that repair set A scores better than 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 better than 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
384
385 """
386 score_a = lab_inventory._score_repair_set(
387 self._buffer_counts,
388 self._make_history_list(repair_a))
389 score_b = lab_inventory._score_repair_set(
390 self._buffer_counts,
391 self._make_history_list(repair_b))
392 self.assertGreater(score_a, score_b)
393
394
395 def _check_equal(self, repair_a, repair_b):
396 """Test that repair set A scores the same as B.
397
398 Contruct repair sets from `repair_a` and `repair_b`,
399 and score both of them using the pre-existing
400 `self._buffer_counts`. Assert that the score for A is
401 equal to the score for B.
402
403 @param repair_a Input data for repair set A
404 @param repair_b Input data for repair set B
405
406 """
407 score_a = lab_inventory._score_repair_set(
408 self._buffer_counts,
409 self._make_history_list(repair_a))
410 score_b = lab_inventory._score_repair_set(
411 self._buffer_counts,
412 self._make_history_list(repair_b))
413 self.assertEqual(score_a, score_b)
414
415
416 def test_improve_worst_board(self):
417 """Test that improving the worst board improves scoring.
418
419 Construct a buffer counts dictionary with all boards having
420 different counts. Assert that it is both necessary and
421 sufficient to improve the count of the worst board in order
422 to improve the score.
423
424 """
425 self._make_buffer_counts(('lion', 0),
426 ('tiger', 1),
427 ('bear', 2))
428 self._check_better([('lion', 1)], [('tiger', 1)])
429 self._check_better([('lion', 1)], [('bear', 1)])
430 self._check_better([('lion', 1)], [('tiger', 2)])
431 self._check_better([('lion', 1)], [('bear', 2)])
432 self._check_equal([('tiger', 1)], [('bear', 1)])
433
434
435 def test_improve_worst_case_count(self):
436 """Test that improving the number of worst cases improves the score.
437
438 Construct a buffer counts dictionary with all boards having
439 the same counts. Assert that improving two boards is better
440 than improving one. Assert that improving any one board is
441 as good as any other.
442
443 """
444 self._make_buffer_counts(('lion', 0),
445 ('tiger', 0),
446 ('bear', 0))
447 self._check_better([('lion', 1), ('tiger', 1)], [('bear', 2)])
448 self._check_equal([('lion', 2)], [('tiger', 1)])
449 self._check_equal([('tiger', 1)], [('bear', 1)])
450
451
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800452# Each item is the number of DUTs in that status.
453STATUS_CHOICES = (_WORKING, _BROKEN, _UNUSED)
454StatusCounts = collections.namedtuple('StatusCounts', ['good', 'bad', 'unused'])
455# Each item is a StatusCounts tuple specifying the number of DUTs per status in
456# the that pool.
457CRITICAL_POOL = lab_inventory.CRITICAL_POOLS[0]
458SPARE_POOL = lab_inventory.SPARE_POOL
459POOL_CHOICES = (CRITICAL_POOL, SPARE_POOL)
460PoolStatusCounts = collections.namedtuple('PoolStatusCounts',
461 ['critical', 'spare'])
J. Richard Barnette96db3492015-03-27 17:23:52 -0700462
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800463def create_inventory(data, by_board=True):
464 """Initialize a `_LabInventory` instance for testing.
465
466 This function allows the construction of a complete `_LabInventory` object
467 from a simplified input representation.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700468
469 A single 'critical pool' is arbitrarily chosen for purposes of
470 testing; there's no coverage for testing arbitrary combinations
471 in more than one critical pool.
472
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800473 @param data: dict {key: PoolStatusCounts}. key_type determines what key
474 represents.
475 @param by_board: Whether to create LabInventory based on board. This
476 function can be used to create _LabInventory based on exactly one of
477 board or model. When creating by board, a dummy model is chosen and
478 vice-versa.
479
480 @returns: lab_inventory._LabInventory object.
481
J. Richard Barnette96db3492015-03-27 17:23:52 -0700482 """
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800483 histories = []
484 for key, counts in data.iteritems():
485 for p, pool in enumerate(POOL_CHOICES):
486 for s, status in enumerate(STATUS_CHOICES):
487 if by_board:
488 board = key
489 model = 'dummy_model'
490 else:
491 board = 'dummy_board'
492 model = key
493 histories.extend(
494 [_FakeHostHistory(board, model, pool, status)] *
495 counts[p][s])
496 inventory = lab_inventory._LabInventory(histories)
497 return inventory
J. Richard Barnette96db3492015-03-27 17:23:52 -0700498
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800499class LabInventoryTests(unittest.TestCase):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700500 """Tests for the basic functions of `_LabInventory`.
501
502 Contains basic coverage to show that after an inventory is
503 created and DUTs with known status are added, the inventory
504 counts match the counts of the added DUTs.
505
J. Richard Barnette96db3492015-03-27 17:23:52 -0700506 """
507
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700508 # _BOARD_LIST - A list of sample board names for use in testing.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700509
510 _BOARD_LIST = [
511 'lion',
512 'tiger',
513 'bear',
514 'aardvark',
515 'platypus',
516 'echidna',
517 'elephant',
518 'giraffe',
519 ]
520
J. Richard Barnettef6839282015-06-01 16:00:35 -0700521
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800522 def _check_inventory_details(self, inventory, data, by_board=True):
523 """Some common detailed inventory checks.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700524
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800525 The checks here are common to many tests below. At the same time, thsese
526 checks are intentionally dumb -- if you need complex logic to figure out
527 what to check, explicitly check for the final value instead of
528 duplicating logic in the test.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700529
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800530 @param inventory: _LabInventory object to check.
531 @param data Inventory data to check against. Same type as
532 `create_inventory`.
533 @param by_board: Whether data is keyed by board (or by model). See
534 create_inventory.
J. Richard Barnettef6839282015-06-01 16:00:35 -0700535
J. Richard Barnette96db3492015-03-27 17:23:52 -0700536 """
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800537 if by_board:
538 inventory_summary = inventory.by_board
539 else:
540 inventory_summary = inventory.by_model
541 self.assertEqual(set(inventory_summary.keys()), set(data.keys()))
542 for key, histories in inventory_summary.iteritems():
543 calculated_counts = PoolStatusCounts(
544 StatusCounts(
545 histories.get_working(CRITICAL_POOL),
546 histories.get_broken(CRITICAL_POOL),
547 histories.get_idle(CRITICAL_POOL),
548 ),
549 StatusCounts(
550 histories.get_working(SPARE_POOL),
551 histories.get_broken(SPARE_POOL),
552 histories.get_idle(SPARE_POOL),
553 ),
554 )
555 self.assertEqual(data[key], calculated_counts)
556
557 self.assertEqual(len(histories.get_working_list()),
558 sum([p.good for p in data[key]]))
559 self.assertEqual(len(histories.get_broken_list()),
560 sum([p.bad for p in data[key]]))
561 self.assertEqual(len(histories.get_idle_list()),
562 sum([p.unused for p in data[key]]))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700563
564
565 def test_empty(self):
566 """Test counts when there are no DUTs recorded."""
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800567 inventory = create_inventory({})
568 self.assertEqual(inventory.get_num_duts(), 0)
569 self.assertEqual(inventory.get_num_boards(), 0)
570 self.assertEqual(inventory.get_managed_boards(), set())
571 self._check_inventory_details(inventory, {})
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700572
573
574 def test_missing_board(self):
575 """Test handling when the board is `None`."""
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800576 inventory = create_inventory({
577 None: PoolStatusCounts(
578 StatusCounts(1, 1, 1),
579 StatusCounts(1, 1, 1),
580 ),
581 })
582 self.assertEqual(inventory.get_num_duts(), 0)
583 self.assertEqual(inventory.get_num_boards(), 0)
584 self.assertEqual(inventory.get_managed_boards(), set())
585 self._check_inventory_details(inventory, {})
J. Richard Barnette96db3492015-03-27 17:23:52 -0700586
587
588 def test_board_counts(self):
589 """Test counts for various numbers of boards."""
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800590 for board_count in [1, 2, len(self._BOARD_LIST)]:
591 self.parameterized_test_board_count(board_count)
592
593
594 def parameterized_test_board_count(self, board_count):
595 """Parameterized test for testing a specific number of boards."""
596 self.longMessage = True
597 msg = '[board_count: %s]' % (board_count,)
598 boards = self._BOARD_LIST[:board_count]
599 data = {
600 b: PoolStatusCounts(
601 StatusCounts(1, 1, 1),
602 StatusCounts(1, 1, 1),
603 )
604 for b in boards
605 }
606 inventory = create_inventory(data)
607 self.assertEqual(inventory.get_num_duts(), 6 * board_count, msg)
608 self.assertEqual(inventory.get_num_boards(), board_count, msg)
609 self.assertEqual(inventory.get_managed_boards(), set(boards), msg)
610 self._check_inventory_details(inventory, data, msg)
611
J. Richard Barnette96db3492015-03-27 17:23:52 -0700612
613
614 def test_single_dut_counts(self):
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800615 """Test counts when there is a single DUT per board, and it is good."""
616 for counts in [
617 PoolStatusCounts(StatusCounts(1, 0, 0), StatusCounts(0, 0, 0)),
618 PoolStatusCounts(StatusCounts(0, 1, 0), StatusCounts(0, 0, 0)),
619 PoolStatusCounts(StatusCounts(0, 0, 1), StatusCounts(0, 0, 0)),
620 PoolStatusCounts(StatusCounts(0, 0, 0), StatusCounts(1, 0, 0)),
621 PoolStatusCounts(StatusCounts(0, 0, 0), StatusCounts(0, 1, 0)),
622 PoolStatusCounts(StatusCounts(0, 0, 0), StatusCounts(0, 0, 1)),
623 ]:
624 self.parameterized_test_single_dut_counts(counts)
625
626 def parameterized_test_single_dut_counts(self, counts):
627 """Parmeterized test for single dut counts."""
628 self.longMessage = True
629 board = self._BOARD_LIST[0]
630 data = {board: counts}
631 msg = '[data: %s]' % (data,)
632 inventory = create_inventory(data)
633 self.assertEqual(inventory.get_num_duts(), 1, msg)
634 self.assertEqual(inventory.get_num_boards(), 1, msg)
635 self.assertEqual(inventory.get_managed_boards(), set(), msg)
636 self._check_inventory_details(inventory, data, msg)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700637
638
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800639# BOARD_MESSAGE_TEMPLATE -
J. Richard Barnette96db3492015-03-27 17:23:52 -0700640# This is a sample of the output text produced by
641# _generate_board_inventory_message(). This string is parsed by the
642# tests below to construct a sample inventory that should produce
643# the output, and then the output is generated and checked against
644# this original sample.
645#
646# Constructing inventories from parsed sample text serves two
647# related purposes:
648# - It provides a way to see what the output should look like
649# without having to run the script.
650# - It helps make sure that a human being will actually look at
651# the output to see that it's basically readable.
652# This should also help prevent test bugs caused by writing tests
653# that simply parrot the original output generation code.
654
655_BOARD_MESSAGE_TEMPLATE = '''
xixuan12ce04f2016-03-10 13:16:30 -0800656Board Avail Bad Idle Good Spare Total
657lion -1 13 2 11 12 26
658tiger -1 5 2 9 4 16
659bear 0 5 2 10 5 17
660platypus 4 2 2 20 6 24
661aardvark 7 2 2 6 9 10
J. Richard Barnette96db3492015-03-27 17:23:52 -0700662'''
663
664
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800665class BoardInventoryTests(unittest.TestCase):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700666 """Tests for `_generate_board_inventory_message()`.
667
668 The tests create various test inventories designed to match the
669 counts in `_BOARD_MESSAGE_TEMPLATE`, and asserts that the
670 generated message text matches the original message text.
671
672 Message text is represented as a list of strings, split on the
673 `'\n'` separator.
674
675 """
676
677 def setUp(self):
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800678 self.maxDiff = None
679 lines = [x.strip() for x in _BOARD_MESSAGE_TEMPLATE.split('\n') if
680 x.strip()]
681 self._header, self._board_lines = lines[0], lines[1:]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700682 self._board_data = []
683 for l in self._board_lines:
684 items = l.split()
685 board = items[0]
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800686 bad, idle, good, spare = [int(x) for x in items[2:-1]]
xixuan12ce04f2016-03-10 13:16:30 -0800687 self._board_data.append((board, (good, bad, idle, spare)))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700688
689
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800690
J. Richard Barnette96db3492015-03-27 17:23:52 -0700691 def _make_minimum_spares(self, counts):
692 """Create a counts tuple with as few spare DUTs as possible."""
xixuan12ce04f2016-03-10 13:16:30 -0800693 good, bad, idle, spares = counts
694 if spares > bad + idle:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800695 return PoolStatusCounts(
696 StatusCounts(good + bad +idle - spares, 0, 0),
697 StatusCounts(spares - bad - idle, bad, idle),
698 )
xixuan12ce04f2016-03-10 13:16:30 -0800699 elif spares < bad:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800700 return PoolStatusCounts(
701 StatusCounts(good, bad - spares, idle),
702 StatusCounts(0, spares, 0),
703 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700704 else:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800705 return PoolStatusCounts(
706 StatusCounts(good, 0, idle + bad - spares),
707 StatusCounts(0, bad, spares - bad),
708 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700709
710
711 def _make_maximum_spares(self, counts):
712 """Create a counts tuple with as many spare DUTs as possible."""
xixuan12ce04f2016-03-10 13:16:30 -0800713 good, bad, idle, spares = counts
J. Richard Barnette96db3492015-03-27 17:23:52 -0700714 if good > spares:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800715 return PoolStatusCounts(
716 StatusCounts(good - spares, bad, idle),
717 StatusCounts(spares, 0, 0),
718 )
xixuan12ce04f2016-03-10 13:16:30 -0800719 elif good + bad > spares:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800720 return PoolStatusCounts(
721 StatusCounts(0, good + bad - spares, idle),
722 StatusCounts(good, spares - good, 0),
723 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700724 else:
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800725 return PoolStatusCounts(
726 StatusCounts(0, 0, good + bad + idle - spares),
727 StatusCounts(good, bad, spares - good - bad),
728 )
J. Richard Barnette96db3492015-03-27 17:23:52 -0700729
730
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800731 def _check_message(self, message):
732 """Checks that message approximately matches expected string."""
733 message = [x.strip() for x in message.split('\n') if x.strip()]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700734 self.assertIn(self._header, message)
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800735 body = message[message.index(self._header) + 1:]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700736 self.assertEqual(body, self._board_lines)
737
738
739 def test_minimum_spares(self):
740 """Test message generation when the spares pool is low."""
741 data = {
742 board: self._make_minimum_spares(counts)
743 for board, counts in self._board_data
744 }
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800745 inventory = create_inventory(data)
746 message = lab_inventory._generate_board_inventory_message(inventory)
747 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700748
749 def test_maximum_spares(self):
750 """Test message generation when the critical pool is low."""
751 data = {
752 board: self._make_maximum_spares(counts)
753 for board, counts in self._board_data
754 }
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800755 inventory = create_inventory(data)
756 message = lab_inventory._generate_board_inventory_message(inventory)
757 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700758
759
760 def test_ignore_no_spares(self):
761 """Test that messages ignore boards with no spare pool."""
762 data = {
763 board: self._make_maximum_spares(counts)
764 for board, counts in self._board_data
765 }
xixuan12ce04f2016-03-10 13:16:30 -0800766 data['elephant'] = ((5, 4, 0), (0, 0, 0))
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800767 inventory = create_inventory(data)
768 message = lab_inventory._generate_board_inventory_message(inventory)
769 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700770
771
772 def test_ignore_no_critical(self):
773 """Test that messages ignore boards with no critical pools."""
774 data = {
775 board: self._make_maximum_spares(counts)
776 for board, counts in self._board_data
777 }
xixuan12ce04f2016-03-10 13:16:30 -0800778 data['elephant'] = ((0, 0, 0), (1, 5, 1))
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800779 inventory = create_inventory(data)
780 message = lab_inventory._generate_board_inventory_message(inventory)
781 self._check_message(message)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700782
783
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800784 def test_ignore_no_bad(self):
785 """Test that messages ignore boards with no bad DUTs."""
786 data = {
787 board: self._make_maximum_spares(counts)
788 for board, counts in self._board_data
789 }
xixuan12ce04f2016-03-10 13:16:30 -0800790 data['elephant'] = ((5, 0, 1), (5, 0, 1))
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800791 inventory = create_inventory(data)
792 message = lab_inventory._generate_board_inventory_message(inventory)
793 self._check_message(message)
J. Richard Barnetteea5a4ba2016-02-18 16:34:50 -0800794
795
xixuan12ce04f2016-03-10 13:16:30 -0800796class _PoolInventoryTestBase(unittest.TestCase):
797 """Parent class for tests relating to generating pool inventory messages.
798
799 Func `setUp` in the class parses a given |message_template| to obtain
800 header and body.
801 """
802 def _read_template(self, message_template):
803 """Read message template for PoolInventoryTest and IdleInventoryTest.
804
805 @param message_template: the input template to be parsed into: header
806 and content (report_lines).
807
808 """
809 message_lines = message_template.split('\n')
810 self._header = message_lines[1]
811 self._report_lines = message_lines[2:-1]
812
813
814 def _check_report_no_info(self, text):
815 """Test a message body containing no reported info.
816
817 The input `text` was created from a query to an inventory, which has
818 no objects meet the query and leads to an `empty` return. Assert that
819 the text consists of a single line starting with '(' and ending with ')'.
820
821 @param text: Message body text to be tested.
822
823 """
824 self.assertTrue(len(text) == 1 and
825 text[0][0] == '(' and
826 text[0][-1] == ')')
827
828
829 def _check_report(self, text):
830 """Test a message against the passed |expected_content|.
831
832 @param text: Message body text to be tested.
833 @param expected_content: The ground-truth content to be compared with.
834
835 """
836 self.assertEqual(text, self._report_lines)
837
838
J. Richard Barnette96db3492015-03-27 17:23:52 -0700839# _POOL_MESSAGE_TEMPLATE -
840# This is a sample of the output text produced by
841# _generate_pool_inventory_message(). This string is parsed by the
842# tests below to construct a sample inventory that should produce
843# the output, and then the output is generated and checked against
844# this original sample.
845#
846# See the comments on _BOARD_MESSAGE_TEMPLATE above for the
847# rationale on using sample text in this way.
848
849_POOL_MESSAGE_TEMPLATE = '''
xixuan12ce04f2016-03-10 13:16:30 -0800850Board Bad Idle Good Total
851lion 5 2 6 13
852tiger 4 1 5 10
853bear 3 0 7 10
854aardvark 2 0 0 2
855platypus 1 1 1 3
J. Richard Barnette96db3492015-03-27 17:23:52 -0700856'''
857
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700858_POOL_ADMIN_URL = 'http://go/cros-manage-duts'
859
860
xixuan12ce04f2016-03-10 13:16:30 -0800861class PoolInventoryTests(_PoolInventoryTestBase):
J. Richard Barnette96db3492015-03-27 17:23:52 -0700862 """Tests for `_generate_pool_inventory_message()`.
863
864 The tests create various test inventories designed to match the
865 counts in `_POOL_MESSAGE_TEMPLATE`, and assert that the
866 generated message text matches the format established in the
867 original message text.
868
869 The output message text is parsed against the following grammar:
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700870 <message> -> <intro> <pool> { "blank line" <pool> }
871 <intro> ->
872 Instructions to depty mentioning the admin page URL
873 A blank line
J. Richard Barnette96db3492015-03-27 17:23:52 -0700874 <pool> ->
875 <description>
876 <header line>
877 <message body>
878 <description> ->
879 Any number of lines describing one pool
880 <header line> ->
881 The header line from `_POOL_MESSAGE_TEMPLATE`
882 <message body> ->
883 Any number of non-blank lines
884
885 After parsing messages into the parts described above, various
886 assertions are tested against the parsed output, including
887 that the message body matches the body from
888 `_POOL_MESSAGE_TEMPLATE`.
889
890 Parse message text is represented as a list of strings, split on
891 the `'\n'` separator.
892
893 """
J. Richard Barnette96db3492015-03-27 17:23:52 -0700894 def setUp(self):
xixuan12ce04f2016-03-10 13:16:30 -0800895 super(PoolInventoryTests, self)._read_template(_POOL_MESSAGE_TEMPLATE)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700896 self._board_data = []
xixuan12ce04f2016-03-10 13:16:30 -0800897 for l in self._report_lines:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700898 items = l.split()
899 board = items[0]
J. Richard Barnette96db3492015-03-27 17:23:52 -0700900 bad = int(items[1])
xixuan12ce04f2016-03-10 13:16:30 -0800901 idle = int(items[2])
902 good = int(items[3])
903 self._board_data.append((board, (good, bad, idle)))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700904
905
906 def _create_histories(self, pools, board_data):
907 """Return a list suitable to create a `_LabInventory` object.
908
909 Creates a list of `_FakeHostHistory` objects that can be
910 used to create a lab inventory. `pools` is a list of strings
911 naming pools, and `board_data` is a list of tuples of the
912 form
913 `(board, (goodcount, badcount))`
914 where
915 `board` is a board name.
916 `goodcount` is the number of working DUTs in the pool.
917 `badcount` is the number of broken DUTs in the pool.
918
919 @param pools List of pools for which to create
920 histories.
921 @param board_data List of tuples containing boards and DUT
922 counts.
923 @return A list of `_FakeHostHistory` objects that can be
924 used to create a `_LabInventory` object.
925
926 """
927 histories = []
xixuan12ce04f2016-03-10 13:16:30 -0800928 status_choices = (_WORKING, _BROKEN, _UNUSED)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700929 for pool in pools:
930 for board, counts in board_data:
931 for status, count in zip(status_choices, counts):
932 for x in range(0, count):
933 histories.append(
Prathmesh Prabhu58aede82017-11-09 13:34:25 -0800934 _FakeHostHistory(board, None, pool, status))
J. Richard Barnette96db3492015-03-27 17:23:52 -0700935 return histories
936
937
938 def _parse_pool_summaries(self, histories):
939 """Parse message output according to the grammar above.
940
941 Create a lab inventory from the given `histories`, and
942 generate the pool inventory message. Then parse the message
943 and return a dictionary mapping each pool to the message
944 body parsed after that pool.
945
946 Tests the following assertions:
947 * Each <description> contains a mention of exactly one
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700948 pool in the `CRITICAL_POOLS` list.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700949 * Each pool is mentioned in exactly one <description>.
950 Note that the grammar requires the header to appear once
951 for each pool, so the parsing implicitly asserts that the
952 output contains the header.
953
954 @param histories Input used to create the test
955 `_LabInventory` object.
956 @return A dictionary mapping board names to the output
957 (a list of lines) for the board.
958
959 """
xixuan12ce04f2016-03-10 13:16:30 -0800960 inventory = lab_inventory._LabInventory(histories)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700961 message = lab_inventory._generate_pool_inventory_message(
xixuan12ce04f2016-03-10 13:16:30 -0800962 inventory).split('\n')
Kevin Chengcf0ad2b2016-04-19 14:51:39 -0700963 poolset = set(lab_inventory.CRITICAL_POOLS)
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700964 seen_url = False
965 seen_intro = False
J. Richard Barnette96db3492015-03-27 17:23:52 -0700966 description = ''
967 board_text = {}
968 current_pool = None
969 for line in message:
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700970 if not seen_url:
971 if _POOL_ADMIN_URL in line:
972 seen_url = True
973 elif not seen_intro:
974 if not line:
975 seen_intro = True
976 elif current_pool is None:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700977 if line == self._header:
978 pools_mentioned = [p for p in poolset
979 if p in description]
980 self.assertEqual(len(pools_mentioned), 1)
981 current_pool = pools_mentioned[0]
982 description = ''
983 board_text[current_pool] = []
984 poolset.remove(current_pool)
985 else:
986 description += line
987 else:
988 if line:
989 board_text[current_pool].append(line)
990 else:
991 current_pool = None
992 self.assertEqual(len(poolset), 0)
993 return board_text
994
995
J. Richard Barnette96db3492015-03-27 17:23:52 -0700996 def test_no_shortages(self):
997 """Test correct output when no pools have shortages."""
998 board_text = self._parse_pool_summaries([])
999 for text in board_text.values():
xixuan12ce04f2016-03-10 13:16:30 -08001000 self._check_report_no_info(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001001
1002
1003 def test_one_pool_shortage(self):
1004 """Test correct output when exactly one pool has a shortage."""
Kevin Chengcf0ad2b2016-04-19 14:51:39 -07001005 for pool in lab_inventory.CRITICAL_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -07001006 histories = self._create_histories((pool,),
1007 self._board_data)
1008 board_text = self._parse_pool_summaries(histories)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -07001009 for checkpool in lab_inventory.CRITICAL_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -07001010 text = board_text[checkpool]
1011 if checkpool == pool:
xixuan12ce04f2016-03-10 13:16:30 -08001012 self._check_report(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001013 else:
xixuan12ce04f2016-03-10 13:16:30 -08001014 self._check_report_no_info(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001015
1016
1017 def test_all_pool_shortages(self):
1018 """Test correct output when all pools have a shortage."""
1019 histories = []
Kevin Chengcf0ad2b2016-04-19 14:51:39 -07001020 for pool in lab_inventory.CRITICAL_POOLS:
J. Richard Barnette96db3492015-03-27 17:23:52 -07001021 histories.extend(
1022 self._create_histories((pool,),
1023 self._board_data))
1024 board_text = self._parse_pool_summaries(histories)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -07001025 for pool in lab_inventory.CRITICAL_POOLS:
xixuan12ce04f2016-03-10 13:16:30 -08001026 self._check_report(board_text[pool])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001027
1028
1029 def test_full_board_ignored(self):
1030 """Test that boards at full strength are not reported."""
Kevin Chengcf0ad2b2016-04-19 14:51:39 -07001031 pool = lab_inventory.CRITICAL_POOLS[0]
xixuan12ce04f2016-03-10 13:16:30 -08001032 full_board = [('echidna', (5, 0, 0))]
J. Richard Barnette96db3492015-03-27 17:23:52 -07001033 histories = self._create_histories((pool,),
1034 full_board)
1035 text = self._parse_pool_summaries(histories)[pool]
xixuan12ce04f2016-03-10 13:16:30 -08001036 self._check_report_no_info(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001037 board_data = self._board_data + full_board
1038 histories = self._create_histories((pool,), board_data)
1039 text = self._parse_pool_summaries(histories)[pool]
xixuan12ce04f2016-03-10 13:16:30 -08001040 self._check_report(text)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001041
1042
1043 def test_spare_pool_ignored(self):
1044 """Test that reporting ignores the spare pool inventory."""
Kevin Chengcf0ad2b2016-04-19 14:51:39 -07001045 spare_pool = lab_inventory.SPARE_POOL
xixuan12ce04f2016-03-10 13:16:30 -08001046 spare_data = self._board_data + [('echidna', (0, 5, 0))]
J. Richard Barnette96db3492015-03-27 17:23:52 -07001047 histories = self._create_histories((spare_pool,),
1048 spare_data)
1049 board_text = self._parse_pool_summaries(histories)
Kevin Chengcf0ad2b2016-04-19 14:51:39 -07001050 for pool in lab_inventory.CRITICAL_POOLS:
xixuan12ce04f2016-03-10 13:16:30 -08001051 self._check_report_no_info(board_text[pool])
1052
1053
1054_IDLE_MESSAGE_TEMPLATE = '''
1055Hostname Board Pool
1056chromeos4-row12-rack4-host7 tiger bvt
1057chromeos1-row3-rack1-host2 lion bvt
1058chromeos3-row2-rack2-host5 lion cq
1059chromeos2-row7-rack3-host11 platypus suites
1060'''
1061
1062
1063class IdleInventoryTests(_PoolInventoryTestBase):
1064 """Tests for `_generate_idle_inventory_message()`.
1065
1066 The tests create idle duts that match the counts and pool in
1067 `_IDLE_MESSAGE_TEMPLATE`. In test, it asserts that the generated
1068 idle message text matches the format established in
1069 `_IDLE_MESSAGE_TEMPLATE`.
1070
1071 Parse message text is represented as a list of strings, split on
1072 the `'\n'` separator.
1073
1074 """
1075
1076 def setUp(self):
1077 super(IdleInventoryTests, self)._read_template(_IDLE_MESSAGE_TEMPLATE)
1078 self._host_data = []
1079 for h in self._report_lines:
1080 items = h.split()
1081 hostname = items[0]
1082 board = items[1]
1083 pool = items[2]
1084 self._host_data.append((hostname, board, pool))
1085 self._histories = []
Prathmesh Prabhu58aede82017-11-09 13:34:25 -08001086 self._histories.append(_FakeHostHistory('echidna', None, 'bvt',
1087 _BROKEN))
1088 self._histories.append(_FakeHostHistory('lion', None, 'bvt', _WORKING))
xixuan12ce04f2016-03-10 13:16:30 -08001089
1090
1091 def _add_idles(self):
1092 """Add idle duts from `_IDLE_MESSAGE_TEMPLATE`."""
1093 idle_histories = [_FakeHostHistory(
Prathmesh Prabhu58aede82017-11-09 13:34:25 -08001094 board, None, pool, _UNUSED, hostname=hostname)
xixuan12ce04f2016-03-10 13:16:30 -08001095 for hostname, board, pool in self._host_data]
1096 self._histories.extend(idle_histories)
1097
1098
1099 def _check_header(self, text):
1100 """Check whether header in the template `_IDLE_MESSAGE_TEMPLATE` is in
1101 passed text."""
1102 self.assertIn(self._header, text)
1103
1104
1105 def _get_idle_message(self, histories):
1106 """Generate idle inventory and obtain its message.
1107
1108 @param histories: Used to create lab inventory.
1109
1110 @return the generated idle message.
1111
1112 """
1113 inventory = lab_inventory._LabInventory(histories)
1114 message = lab_inventory._generate_idle_inventory_message(
1115 inventory).split('\n')
1116 return message
1117
1118
1119 def test_check_idle_inventory(self):
1120 """Test that reporting all the idle DUTs for every pool, sorted by
Kevin Chengcf0ad2b2016-04-19 14:51:39 -07001121 lab_inventory.MANAGED_POOLS.
xixuan12ce04f2016-03-10 13:16:30 -08001122 """
1123 self._add_idles()
1124
1125 message = self._get_idle_message(self._histories)
1126 self._check_header(message)
1127 self._check_report(message[message.index(self._header) + 1 :])
1128
1129
1130 def test_no_idle_inventory(self):
1131 """Test that reporting no idle DUTs."""
1132 message = self._get_idle_message(self._histories)
1133 self._check_header(message)
1134 self._check_report_no_info(
1135 message[message.index(self._header) + 1 :])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001136
1137
1138class CommandParsingTests(unittest.TestCase):
1139 """Tests for command line argument parsing in `_parse_command()`."""
1140
J. Richard Barnette02e82432015-10-13 16:02:47 -07001141 _NULL_NOTIFY = ['--board-notify=', '--pool-notify=']
1142
J. Richard Barnette96db3492015-03-27 17:23:52 -07001143 def setUp(self):
1144 dirpath = '/usr/local/fubar'
1145 self._command_path = os.path.join(dirpath,
1146 'site_utils',
1147 'arglebargle')
1148 self._logdir = os.path.join(dirpath, lab_inventory._LOGDIR)
1149
1150
J. Richard Barnette02e82432015-10-13 16:02:47 -07001151 def _parse_arguments(self, argv, notify=_NULL_NOTIFY):
1152 full_argv = [self._command_path] + argv + notify
J. Richard Barnette96db3492015-03-27 17:23:52 -07001153 return lab_inventory._parse_command(full_argv)
1154
1155
J. Richard Barnette02e82432015-10-13 16:02:47 -07001156 def _check_non_notify_defaults(self, notify_option):
1157 arguments = self._parse_arguments([], notify=[notify_option])
1158 self.assertEqual(arguments.duration,
1159 lab_inventory._DEFAULT_DURATION)
1160 self.assertFalse(arguments.debug)
1161 self.assertEqual(arguments.logdir, self._logdir)
1162 self.assertEqual(arguments.boardnames, [])
1163 return arguments
1164
1165
1166 def test_empty_arguments(self):
1167 """Test that an empty argument list is an error."""
1168 arguments = self._parse_arguments([], notify=[])
1169 self.assertIsNone(arguments)
1170
1171
J. Richard Barnette96db3492015-03-27 17:23:52 -07001172 def test_argument_defaults(self):
1173 """Test that option defaults match expectations."""
J. Richard Barnette02e82432015-10-13 16:02:47 -07001174 arguments = self._check_non_notify_defaults(self._NULL_NOTIFY[0])
1175 self.assertEqual(arguments.board_notify, [''])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001176 self.assertEqual(arguments.pool_notify, [])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001177 arguments = self._check_non_notify_defaults(self._NULL_NOTIFY[1])
1178 self.assertEqual(arguments.board_notify, [])
1179 self.assertEqual(arguments.pool_notify, [''])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001180
1181
1182 def test_board_arguments(self):
1183 """Test that non-option arguments are returned in `boardnames`."""
1184 boardlist = ['aardvark', 'echidna']
1185 arguments = self._parse_arguments(boardlist)
1186 self.assertEqual(arguments.boardnames, boardlist)
1187
1188
J. Richard Barnette02e82432015-10-13 16:02:47 -07001189 def test_debug_option(self):
1190 """Test parsing of the `--debug` option."""
1191 arguments = self._parse_arguments(['--debug'])
1192 self.assertTrue(arguments.debug)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001193
1194
1195 def test_duration(self):
1196 """Test parsing of the `--duration` option."""
1197 arguments = self._parse_arguments(['--duration', '1'])
1198 self.assertEqual(arguments.duration, 1)
1199 arguments = self._parse_arguments(['--duration', '11'])
1200 self.assertEqual(arguments.duration, 11)
1201 arguments = self._parse_arguments(['-d', '1'])
1202 self.assertEqual(arguments.duration, 1)
1203 arguments = self._parse_arguments(['-d', '11'])
1204 self.assertEqual(arguments.duration, 11)
1205
1206
1207 def _check_email_option(self, option, getlist):
1208 """Test parsing of e-mail address options.
1209
1210 This is a helper function to test the `--board-notify` and
1211 `--pool-notify` options. It tests the following cases:
1212 * `--option a1` gives the list [a1]
1213 * `--option ' a1 '` gives the list [a1]
1214 * `--option a1 --option a2` gives the list [a1, a2]
1215 * `--option a1,a2` gives the list [a1, a2]
1216 * `--option 'a1, a2'` gives the list [a1, a2]
1217
1218 @param option The option to be tested.
1219 @param getlist A function to return the option's value from
1220 parsed command line arguments.
1221
1222 """
1223 a1 = 'mumble@mumbler.com'
1224 a2 = 'bumble@bumbler.org'
J. Richard Barnette02e82432015-10-13 16:02:47 -07001225 arguments = self._parse_arguments([option, a1], notify=[])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001226 self.assertEqual(getlist(arguments), [a1])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001227 arguments = self._parse_arguments([option, ' ' + a1 + ' '],
1228 notify=[])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001229 self.assertEqual(getlist(arguments), [a1])
J. Richard Barnette02e82432015-10-13 16:02:47 -07001230 arguments = self._parse_arguments([option, a1, option, a2],
1231 notify=[])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001232 self.assertEqual(getlist(arguments), [a1, a2])
1233 arguments = self._parse_arguments(
J. Richard Barnette02e82432015-10-13 16:02:47 -07001234 [option, ','.join([a1, a2])], notify=[])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001235 self.assertEqual(getlist(arguments), [a1, a2])
1236 arguments = self._parse_arguments(
J. Richard Barnette02e82432015-10-13 16:02:47 -07001237 [option, ', '.join([a1, a2])], notify=[])
J. Richard Barnette96db3492015-03-27 17:23:52 -07001238 self.assertEqual(getlist(arguments), [a1, a2])
1239
1240
1241 def test_board_notify(self):
1242 """Test parsing of the `--board-notify` option."""
1243 self._check_email_option('--board-notify',
1244 lambda a: a.board_notify)
1245
1246
1247 def test_pool_notify(self):
1248 """Test parsing of the `--pool-notify` option."""
1249 self._check_email_option('--pool-notify',
1250 lambda a: a.pool_notify)
1251
1252
1253 def test_pool_logdir(self):
1254 """Test parsing of the `--logdir` option."""
1255 logdir = '/usr/local/whatsis/logs'
1256 arguments = self._parse_arguments(['--logdir', logdir])
1257 self.assertEqual(arguments.logdir, logdir)
1258
1259
1260if __name__ == '__main__':
J. Richard Barnettef60a1ee2015-06-02 10:52:37 -07001261 # Some of the functions we test log messages. Prevent those
1262 # messages from showing up in test output.
1263 logging.getLogger().setLevel(logging.CRITICAL)
J. Richard Barnette96db3492015-03-27 17:23:52 -07001264 unittest.main()