blob: 3d7a3566ca8be85cb575903d6afb5dd4b665f638 [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
J. Richard Barnettef60a1ee2015-06-02 10:52:37 -07006import logging
J. Richard Barnette96db3492015-03-27 17:23:52 -07007import os
8import unittest
9
10import common
11from autotest_lib.site_utils import lab_inventory
12from autotest_lib.site_utils import status_history
13
14
15class _FakeHostHistory(object):
16 """Class to mock `HostJobHistory` for testing."""
17
18 def __init__(self, board, pool, status):
19 self._board = board
20 self._pool = pool
21 self._status = status
22
23
J. Richard Barnette3d0590a2015-04-29 12:56:12 -070024 @property
25 def host_board(self):
J. Richard Barnette96db3492015-03-27 17:23:52 -070026 """Return the recorded board."""
27 return self._board
28
29
J. Richard Barnette3d0590a2015-04-29 12:56:12 -070030 @property
31 def host_pool(self):
J. Richard Barnette96db3492015-03-27 17:23:52 -070032 """Return the recorded host."""
33 return self._pool
34
35
36 def last_diagnosis(self):
37 """Return the recorded diagnosis."""
38 return self._status, None
39
40
41# Status values that may be returned by `HostJobHistory`.
42#
43# _NON_WORKING_STATUS_LIST - The complete list (as of this writing)
44# of status values that the lab_inventory module treats as
45# "broken".
46# _WORKING - A value that counts as "working" for purposes
47# of the lab_inventory module.
48# _BROKEN - A value that counts as "broken" for the lab_inventory
49# module. Since there's more than one valid choice here, we've
50# picked one to stand for all of them.
51
52_NON_WORKING_STATUS_LIST = [
53 status_history.UNUSED,
54 status_history.BROKEN,
55 status_history.UNKNOWN,
56]
57
58_WORKING = status_history.WORKING
59_BROKEN = _NON_WORKING_STATUS_LIST[0]
60
61
62class PoolCountTests(unittest.TestCase):
63 """Unit tests for class `_PoolCounts`.
64
65 Coverage is quite basic: mostly just enough to make sure every
66 function gets called, and to make sure that the counting knows
67 the difference between 0 and 1.
68
69 The testing also ensures that all known status values that
70 can be returned by `HostJobHistory` are counted as expected.
71
72 """
73
74 def setUp(self):
75 super(PoolCountTests, self).setUp()
76 self._pool_counts = lab_inventory._PoolCounts()
77
78
79 def _add_host(self, status):
80 fake = _FakeHostHistory(
81 None, lab_inventory._SPARE_POOL, status)
82 self._pool_counts.record_host(fake)
83
84
85 def _check_counts(self, working, broken):
86 """Check that pool counts match expectations.
87
88 Checks that `get_working()` and `get_broken()` return the
89 given expected values. Also check that `get_total()` is the
90 sum of working and broken devices.
91
92 @param working The expected total of working devices.
93 @param broken The expected total of broken devices.
94
95 """
96 self.assertEqual(self._pool_counts.get_working(), working)
97 self.assertEqual(self._pool_counts.get_broken(), broken)
98 self.assertEqual(self._pool_counts.get_total(),
99 working + broken)
100
101
102 def test_empty(self):
103 """Test counts when there are no DUTs recorded."""
104 self._check_counts(0, 0)
105
106
107 def test_non_working(self):
108 """Test counting for all non-working status values."""
109 count = 0
110 for status in _NON_WORKING_STATUS_LIST:
111 self._add_host(status)
112 count += 1
113 self._check_counts(0, count)
114
115
116 def test_working_then_broken(self):
117 """Test counts after adding a working and then a broken DUT."""
118 self._add_host(_WORKING)
119 self._check_counts(1, 0)
120 self._add_host(_BROKEN)
121 self._check_counts(1, 1)
122
123
124 def test_broken_then_working(self):
125 """Test counts after adding a broken and then a working DUT."""
126 self._add_host(_BROKEN)
127 self._check_counts(0, 1)
128 self._add_host(_WORKING)
129 self._check_counts(1, 1)
130
131
132class BoardCountTests(unittest.TestCase):
133 """Unit tests for class `_BoardCounts`.
134
135 Coverage is quite basic: just enough to make sure every
136 function gets called, and to make sure that the counting
137 knows the difference between 0 and 1.
138
139 The tests make sure that both individual pool counts and
140 totals are counted correctly.
141
142 """
143
144 def setUp(self):
145 super(BoardCountTests, self).setUp()
146 self._board_counts = lab_inventory._BoardCounts()
147
148
149 def _add_host(self, pool, status):
150 fake = _FakeHostHistory(None, pool, status)
151 self._board_counts.record_host(fake)
152
153
154 def _check_all_counts(self, working, broken):
155 """Check that total counts for all pools match expectations.
156
157 Checks that `get_working()` and `get_broken()` return the
158 given expected values when called without a pool specified.
159 Also check that `get_total()` is the sum of working and
160 broken devices.
161
162 Additionally, call the various functions for all the pools
163 individually, and confirm that the totals across pools match
164 the given expectations.
165
166 @param working The expected total of working devices.
167 @param broken The expected total of broken devices.
168
169 """
170 self.assertEqual(self._board_counts.get_working(), working)
171 self.assertEqual(self._board_counts.get_broken(), broken)
172 self.assertEqual(self._board_counts.get_total(),
173 working + broken)
174 count_working = 0
175 count_broken = 0
176 count_total = 0
177 for pool in lab_inventory._MANAGED_POOLS:
178 count_working += self._board_counts.get_working(pool)
179 count_broken += self._board_counts.get_broken(pool)
180 count_total += self._board_counts.get_total(pool)
181 self.assertEqual(count_working, working)
182 self.assertEqual(count_broken, broken)
183 self.assertEqual(count_total, working + broken)
184
185
186 def _check_pool_counts(self, pool, working, broken):
187 """Check that counts for a given pool match expectations.
188
189 Checks that `get_working()` and `get_broken()` return the
190 given expected values for the given pool. Also check that
191 `get_total()` is the sum of working and broken devices.
192
193 @param pool The pool to be checked.
194 @param working The expected total of working devices.
195 @param broken The expected total of broken devices.
196
197 """
198 self.assertEqual(self._board_counts.get_working(pool),
199 working)
200 self.assertEqual(self._board_counts.get_broken(pool),
201 broken)
202 self.assertEqual(self._board_counts.get_total(pool),
203 working + broken)
204
205
206 def test_empty(self):
207 """Test counts when there are no DUTs recorded."""
208 self._check_all_counts(0, 0)
209 for pool in lab_inventory._MANAGED_POOLS:
210 self._check_pool_counts(pool, 0, 0)
211
212
213 def test_all_working_then_broken(self):
214 """Test counts after adding a working and then a broken DUT.
215
216 For each pool, add first a working, then a broken DUT. After
217 each DUT is added, check counts to confirm the correct values.
218
219 """
220 working = 0
221 broken = 0
222 for pool in lab_inventory._MANAGED_POOLS:
223 self._add_host(pool, _WORKING)
224 working += 1
225 self._check_pool_counts(pool, 1, 0)
226 self._check_all_counts(working, broken)
227 self._add_host(pool, _BROKEN)
228 broken += 1
229 self._check_pool_counts(pool, 1, 1)
230 self._check_all_counts(working, broken)
231
232
233 def test_all_broken_then_working(self):
234 """Test counts after adding a broken and then a working DUT.
235
236 For each pool, add first a broken, then a working DUT. After
237 each DUT is added, check counts to confirm the correct values.
238
239 """
240 working = 0
241 broken = 0
242 for pool in lab_inventory._MANAGED_POOLS:
243 self._add_host(pool, _BROKEN)
244 broken += 1
245 self._check_pool_counts(pool, 0, 1)
246 self._check_all_counts(working, broken)
247 self._add_host(pool, _WORKING)
248 working += 1
249 self._check_pool_counts(pool, 1, 1)
250 self._check_all_counts(working, broken)
251
252
253class _InventoryTests(unittest.TestCase):
254 """Parent class for tests relating to full Lab inventory.
255
256 This class provides a `create_inventory()` method that allows
257 construction of a complete `_LabInventory` object from a
258 restricted input representation. The input representation
259 is a dictionary mapping board names to tuples of this form:
260 `((critgood, critbad), (sparegood, sparebad))`
261 where:
262 `critgood` is a number of working DUTs in one critical pool.
263 `critbad` is a number of broken DUTs in one critical pool.
264 `sparegood` is a number of working DUTs in one critical pool.
265 `sparebad` is a number of broken DUTs in one critical pool.
266
267 A single 'critical pool' is arbitrarily chosen for purposes of
268 testing; there's no coverage for testing arbitrary combinations
269 in more than one critical pool.
270
271 """
272
273 _CRITICAL_POOL = lab_inventory._CRITICAL_POOLS[0]
274 _SPARE_POOL = lab_inventory._SPARE_POOL
275
276 def setUp(self):
277 super(_InventoryTests, self).setUp()
278 self.num_duts = 0
279 self.inventory = None
280
281
282 def create_inventory(self, data):
283 """Initialize a `_LabInventory` instance for testing.
284
285 @param data Representation of Lab inventory data, as
286 described above.
287
288 """
289 histories = []
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700290 self.num_duts = 0
J. Richard Barnette96db3492015-03-27 17:23:52 -0700291 status_choices = (_WORKING, _BROKEN)
292 pools = (self._CRITICAL_POOL, self._SPARE_POOL)
293 for board, counts in data.items():
294 for i in range(0, len(pools)):
295 for j in range(0, len(status_choices)):
296 for x in range(0, counts[i][j]):
297 history = _FakeHostHistory(board,
298 pools[i],
299 status_choices[j])
300 histories.append(history)
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700301 if board is not None:
302 self.num_duts += 1
J. Richard Barnette96db3492015-03-27 17:23:52 -0700303 self.inventory = lab_inventory._LabInventory(histories)
304
305
306class LabInventoryTests(_InventoryTests):
307 """Tests for the basic functions of `_LabInventory`.
308
309 Contains basic coverage to show that after an inventory is
310 created and DUTs with known status are added, the inventory
311 counts match the counts of the added DUTs.
312
313 Test inventory objects are created using the `create_inventory()`
314 method from the parent class.
315
316 """
317
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700318 # _BOARD_LIST - A list of sample board names for use in testing.
J. Richard Barnette96db3492015-03-27 17:23:52 -0700319
320 _BOARD_LIST = [
321 'lion',
322 'tiger',
323 'bear',
324 'aardvark',
325 'platypus',
326 'echidna',
327 'elephant',
328 'giraffe',
329 ]
330
331 def _check_inventory(self, data):
332 """Create a test inventory, and confirm that it's correct.
333
334 Tests these assertions:
335 * The counts of working and broken devices for each
336 board match the numbers from `data`.
337 * That the set of returned boards in the inventory matches
338 the set from `data`.
339 * That the total number of DUTs matches the number from
340 `data`.
341 * That the total number of boards matches the number from
342 `data`.
343
344 @param data Inventory data as for `self.create_inventory()`.
345 """
J. Richard Barnette96db3492015-03-27 17:23:52 -0700346 for b in self.inventory:
347 c = self.inventory[b]
348 calculated_counts = (
349 (c.get_working(self._CRITICAL_POOL),
350 c.get_broken(self._CRITICAL_POOL)),
351 (c.get_working(self._SPARE_POOL),
352 c.get_broken(self._SPARE_POOL)))
353 self.assertEqual(data[b], calculated_counts)
354 self.assertEqual(set(self.inventory.keys()),
355 set(data.keys()))
356 self.assertEqual(self.inventory.get_num_duts(),
357 self.num_duts)
358 self.assertEqual(self.inventory.get_num_boards(),
359 len(data))
360
361
362 def test_empty(self):
363 """Test counts when there are no DUTs recorded."""
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700364 self.create_inventory({})
365 self._check_inventory({})
366
367
368 def test_missing_board(self):
369 """Test handling when the board is `None`."""
370 self.create_inventory({None: ((1, 1), (1, 1))})
J. Richard Barnette96db3492015-03-27 17:23:52 -0700371 self._check_inventory({})
372
373
374 def test_board_counts(self):
375 """Test counts for various numbers of boards."""
376 for nboards in [1, 2, len(self._BOARD_LIST)]:
377 counts = ((1, 1), (1, 1))
378 slice = self._BOARD_LIST[0 : nboards]
379 inventory_data = {
380 board: counts for board in slice
381 }
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700382 self.create_inventory(inventory_data)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700383 self._check_inventory(inventory_data)
384
385
386 def test_single_dut_counts(self):
387 """Test counts when there is a single DUT per board."""
388 testcounts = [
389 ((1, 0), (0, 0)),
390 ((0, 1), (0, 0)),
391 ((0, 0), (1, 0)),
392 ((0, 0), (0, 1)),
393 ]
394 for counts in testcounts:
395 inventory_data = { self._BOARD_LIST[0]: counts }
J. Richard Barnette6948ed32015-05-06 08:57:10 -0700396 self.create_inventory(inventory_data)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700397 self._check_inventory(inventory_data)
398
399
400# _BOARD_MESSAGE_TEMPLATE -
401# This is a sample of the output text produced by
402# _generate_board_inventory_message(). This string is parsed by the
403# tests below to construct a sample inventory that should produce
404# the output, and then the output is generated and checked against
405# this original sample.
406#
407# Constructing inventories from parsed sample text serves two
408# related purposes:
409# - It provides a way to see what the output should look like
410# without having to run the script.
411# - It helps make sure that a human being will actually look at
412# the output to see that it's basically readable.
413# This should also help prevent test bugs caused by writing tests
414# that simply parrot the original output generation code.
415
416_BOARD_MESSAGE_TEMPLATE = '''
417Board Avail Bad Good Spare Total
418lion -1 13 11 12 24
419tiger -1 5 9 4 14
420bear 0 7 10 7 17
421aardvark 1 6 6 7 12
422platypus 2 4 20 6 24
423echidna 6 0 20 6 20
424'''
425
426
427class BoardInventoryTests(_InventoryTests):
428 """Tests for `_generate_board_inventory_message()`.
429
430 The tests create various test inventories designed to match the
431 counts in `_BOARD_MESSAGE_TEMPLATE`, and asserts that the
432 generated message text matches the original message text.
433
434 Message text is represented as a list of strings, split on the
435 `'\n'` separator.
436
437 """
438
439 def setUp(self):
440 super(BoardInventoryTests, self).setUp()
441 # The template string has leading and trailing '\n' that
442 # won't be in the generated output; we strip them out here.
443 message_lines = _BOARD_MESSAGE_TEMPLATE.split('\n')
444 self._header = message_lines[1]
445 self._board_lines = message_lines[2:-1]
446 self._board_data = []
447 for l in self._board_lines:
448 items = l.split()
449 board = items[0]
450 good = int(items[3])
451 bad = int(items[2])
452 spare = int(items[4])
453 self._board_data.append((board, (good, bad, spare)))
454
455
456 def _make_minimum_spares(self, counts):
457 """Create a counts tuple with as few spare DUTs as possible."""
458 good, bad, spares = counts
459 if spares > bad:
460 return ((good + bad - spares, 0),
461 (spares - bad, bad))
462 else:
463 return ((good, bad - spares), (0, spares))
464
465
466 def _make_maximum_spares(self, counts):
467 """Create a counts tuple with as many spare DUTs as possible."""
468 good, bad, spares = counts
469 if good > spares:
470 return ((good - spares, bad), (spares, 0))
471 else:
472 return ((0, good + bad - spares),
473 (good, spares - good))
474
475
476 def _check_board_inventory(self, data):
477 """Test that a test inventory creates the correct message.
478
479 Create a test inventory from `data` using
480 `self.create_inventory()`. Then generate the board inventory
481 output, and test that the output matches
482 `_BOARD_MESSAGE_TEMPLATE`.
483
484 The caller is required to produce data that matches the
485 values in `_BOARD_MESSAGE_TEMPLATE`.
486
487 @param data Inventory data as for `self.create_inventory()`.
488
489 """
490 self.create_inventory(data)
491 message = lab_inventory._generate_board_inventory_message(
492 self.inventory).split('\n')
493 self.assertIn(self._header, message)
494 body = message[message.index(self._header) + 1 :]
495 self.assertEqual(body, self._board_lines)
496
497
498 def test_minimum_spares(self):
499 """Test message generation when the spares pool is low."""
500 data = {
501 board: self._make_minimum_spares(counts)
502 for board, counts in self._board_data
503 }
504 self._check_board_inventory(data)
505
506
507 def test_maximum_spares(self):
508 """Test message generation when the critical pool is low."""
509 data = {
510 board: self._make_maximum_spares(counts)
511 for board, counts in self._board_data
512 }
513 self._check_board_inventory(data)
514
515
516 def test_ignore_no_spares(self):
517 """Test that messages ignore boards with no spare pool."""
518 data = {
519 board: self._make_maximum_spares(counts)
520 for board, counts in self._board_data
521 }
522 data['elephant'] = ((5, 4), (0, 0))
523 self._check_board_inventory(data)
524
525
526 def test_ignore_no_critical(self):
527 """Test that messages ignore boards with no critical pools."""
528 data = {
529 board: self._make_maximum_spares(counts)
530 for board, counts in self._board_data
531 }
532 data['elephant'] = ((0, 0), (1, 5))
533 self._check_board_inventory(data)
534
535
536# _POOL_MESSAGE_TEMPLATE -
537# This is a sample of the output text produced by
538# _generate_pool_inventory_message(). This string is parsed by the
539# tests below to construct a sample inventory that should produce
540# the output, and then the output is generated and checked against
541# this original sample.
542#
543# See the comments on _BOARD_MESSAGE_TEMPLATE above for the
544# rationale on using sample text in this way.
545
546_POOL_MESSAGE_TEMPLATE = '''
547Board Bad Good Total
548lion 5 6 11
549tiger 4 5 9
550bear 3 7 10
551aardvark 2 0 2
552platypus 1 1 2
553'''
554
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700555_POOL_ADMIN_URL = 'http://go/cros-manage-duts'
556
557
J. Richard Barnette96db3492015-03-27 17:23:52 -0700558
559class PoolInventoryTests(unittest.TestCase):
560 """Tests for `_generate_pool_inventory_message()`.
561
562 The tests create various test inventories designed to match the
563 counts in `_POOL_MESSAGE_TEMPLATE`, and assert that the
564 generated message text matches the format established in the
565 original message text.
566
567 The output message text is parsed against the following grammar:
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700568 <message> -> <intro> <pool> { "blank line" <pool> }
569 <intro> ->
570 Instructions to depty mentioning the admin page URL
571 A blank line
J. Richard Barnette96db3492015-03-27 17:23:52 -0700572 <pool> ->
573 <description>
574 <header line>
575 <message body>
576 <description> ->
577 Any number of lines describing one pool
578 <header line> ->
579 The header line from `_POOL_MESSAGE_TEMPLATE`
580 <message body> ->
581 Any number of non-blank lines
582
583 After parsing messages into the parts described above, various
584 assertions are tested against the parsed output, including
585 that the message body matches the body from
586 `_POOL_MESSAGE_TEMPLATE`.
587
588 Parse message text is represented as a list of strings, split on
589 the `'\n'` separator.
590
591 """
592
593 def setUp(self):
594 message_lines = _POOL_MESSAGE_TEMPLATE.split('\n')
595 self._header = message_lines[1]
596 self._board_lines = message_lines[2:-1]
597 self._board_data = []
598 for l in self._board_lines:
599 items = l.split()
600 board = items[0]
601 good = int(items[2])
602 bad = int(items[1])
603 self._board_data.append((board, (good, bad)))
604 self._inventory = None
605
606
607 def _create_histories(self, pools, board_data):
608 """Return a list suitable to create a `_LabInventory` object.
609
610 Creates a list of `_FakeHostHistory` objects that can be
611 used to create a lab inventory. `pools` is a list of strings
612 naming pools, and `board_data` is a list of tuples of the
613 form
614 `(board, (goodcount, badcount))`
615 where
616 `board` is a board name.
617 `goodcount` is the number of working DUTs in the pool.
618 `badcount` is the number of broken DUTs in the pool.
619
620 @param pools List of pools for which to create
621 histories.
622 @param board_data List of tuples containing boards and DUT
623 counts.
624 @return A list of `_FakeHostHistory` objects that can be
625 used to create a `_LabInventory` object.
626
627 """
628 histories = []
629 status_choices = (_WORKING, _BROKEN)
630 for pool in pools:
631 for board, counts in board_data:
632 for status, count in zip(status_choices, counts):
633 for x in range(0, count):
634 histories.append(
635 _FakeHostHistory(board, pool, status))
636 return histories
637
638
639 def _parse_pool_summaries(self, histories):
640 """Parse message output according to the grammar above.
641
642 Create a lab inventory from the given `histories`, and
643 generate the pool inventory message. Then parse the message
644 and return a dictionary mapping each pool to the message
645 body parsed after that pool.
646
647 Tests the following assertions:
648 * Each <description> contains a mention of exactly one
649 pool in the `_CRITICAL_POOLS` list.
650 * Each pool is mentioned in exactly one <description>.
651 Note that the grammar requires the header to appear once
652 for each pool, so the parsing implicitly asserts that the
653 output contains the header.
654
655 @param histories Input used to create the test
656 `_LabInventory` object.
657 @return A dictionary mapping board names to the output
658 (a list of lines) for the board.
659
660 """
661 self._inventory = lab_inventory._LabInventory(histories)
662 message = lab_inventory._generate_pool_inventory_message(
663 self._inventory).split('\n')
664 poolset = set(lab_inventory._CRITICAL_POOLS)
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700665 seen_url = False
666 seen_intro = False
J. Richard Barnette96db3492015-03-27 17:23:52 -0700667 description = ''
668 board_text = {}
669 current_pool = None
670 for line in message:
J. Richard Barnette4845fcf2015-04-20 14:26:25 -0700671 if not seen_url:
672 if _POOL_ADMIN_URL in line:
673 seen_url = True
674 elif not seen_intro:
675 if not line:
676 seen_intro = True
677 elif current_pool is None:
J. Richard Barnette96db3492015-03-27 17:23:52 -0700678 if line == self._header:
679 pools_mentioned = [p for p in poolset
680 if p in description]
681 self.assertEqual(len(pools_mentioned), 1)
682 current_pool = pools_mentioned[0]
683 description = ''
684 board_text[current_pool] = []
685 poolset.remove(current_pool)
686 else:
687 description += line
688 else:
689 if line:
690 board_text[current_pool].append(line)
691 else:
692 current_pool = None
693 self.assertEqual(len(poolset), 0)
694 return board_text
695
696
697 def _check_inventory_no_shortages(self, text):
698 """Test a message body containing no reported shortages.
699
700 The input `text` was created for a pool containing no
701 board shortages. Assert that the text consists of a
702 single line starting with '(' and ending with ')'.
703
704 @param text Message body text to be tested.
705
706 """
707 self.assertTrue(len(text) == 1 and
708 text[0][0] == '(' and
709 text[0][-1] == ')')
710
711
712 def _check_inventory(self, text):
713 """Test a message against `_POOL_MESSAGE_TEMPLATE`.
714
715 Test that the given message text matches the parsed
716 `_POOL_MESSAGE_TEMPLATE`.
717
718 @param text Message body text to be tested.
719
720 """
721 self.assertEqual(text, self._board_lines)
722
723
724 def test_no_shortages(self):
725 """Test correct output when no pools have shortages."""
726 board_text = self._parse_pool_summaries([])
727 for text in board_text.values():
728 self._check_inventory_no_shortages(text)
729
730
731 def test_one_pool_shortage(self):
732 """Test correct output when exactly one pool has a shortage."""
733 for pool in lab_inventory._CRITICAL_POOLS:
734 histories = self._create_histories((pool,),
735 self._board_data)
736 board_text = self._parse_pool_summaries(histories)
737 for checkpool in lab_inventory._CRITICAL_POOLS:
738 text = board_text[checkpool]
739 if checkpool == pool:
740 self._check_inventory(text)
741 else:
742 self._check_inventory_no_shortages(text)
743
744
745 def test_all_pool_shortages(self):
746 """Test correct output when all pools have a shortage."""
747 histories = []
748 for pool in lab_inventory._CRITICAL_POOLS:
749 histories.extend(
750 self._create_histories((pool,),
751 self._board_data))
752 board_text = self._parse_pool_summaries(histories)
753 for pool in lab_inventory._CRITICAL_POOLS:
754 self._check_inventory(board_text[pool])
755
756
757 def test_full_board_ignored(self):
758 """Test that boards at full strength are not reported."""
759 pool = lab_inventory._CRITICAL_POOLS[0]
760 full_board = [('echidna', (5, 0))]
761 histories = self._create_histories((pool,),
762 full_board)
763 text = self._parse_pool_summaries(histories)[pool]
764 self._check_inventory_no_shortages(text)
765 board_data = self._board_data + full_board
766 histories = self._create_histories((pool,), board_data)
767 text = self._parse_pool_summaries(histories)[pool]
768 self._check_inventory(text)
769
770
771 def test_spare_pool_ignored(self):
772 """Test that reporting ignores the spare pool inventory."""
773 spare_pool = lab_inventory._SPARE_POOL
774 spare_data = self._board_data + [('echidna', (0, 5))]
775 histories = self._create_histories((spare_pool,),
776 spare_data)
777 board_text = self._parse_pool_summaries(histories)
778 for pool in lab_inventory._CRITICAL_POOLS:
779 self._check_inventory_no_shortages(board_text[pool])
780
781
782class CommandParsingTests(unittest.TestCase):
783 """Tests for command line argument parsing in `_parse_command()`."""
784
785 def setUp(self):
786 dirpath = '/usr/local/fubar'
787 self._command_path = os.path.join(dirpath,
788 'site_utils',
789 'arglebargle')
790 self._logdir = os.path.join(dirpath, lab_inventory._LOGDIR)
791
792
793 def _parse_arguments(self, argv):
794 full_argv = [self._command_path] + argv
795 return lab_inventory._parse_command(full_argv)
796
797
798 def test_argument_defaults(self):
799 """Test that option defaults match expectations."""
800 arguments = self._parse_arguments([])
801 self.assertEqual(arguments.duration,
802 lab_inventory._DEFAULT_DURATION)
803 self.assertEqual(arguments.board_notify, [])
804 self.assertEqual(arguments.pool_notify, [])
805 self.assertFalse(arguments.print_)
806 self.assertEqual(arguments.logdir, self._logdir)
807 self.assertEqual(arguments.boardnames, [])
808
809
810 def test_board_arguments(self):
811 """Test that non-option arguments are returned in `boardnames`."""
812 boardlist = ['aardvark', 'echidna']
813 arguments = self._parse_arguments(boardlist)
814 self.assertEqual(arguments.boardnames, boardlist)
815
816
817 def test_print_option(self):
818 """Test parsing of the `--print` option."""
819 arguments = self._parse_arguments(['--print'])
820 self.assertTrue(arguments.print_)
821
822
823 def test_duration(self):
824 """Test parsing of the `--duration` option."""
825 arguments = self._parse_arguments(['--duration', '1'])
826 self.assertEqual(arguments.duration, 1)
827 arguments = self._parse_arguments(['--duration', '11'])
828 self.assertEqual(arguments.duration, 11)
829 arguments = self._parse_arguments(['-d', '1'])
830 self.assertEqual(arguments.duration, 1)
831 arguments = self._parse_arguments(['-d', '11'])
832 self.assertEqual(arguments.duration, 11)
833
834
835 def _check_email_option(self, option, getlist):
836 """Test parsing of e-mail address options.
837
838 This is a helper function to test the `--board-notify` and
839 `--pool-notify` options. It tests the following cases:
840 * `--option a1` gives the list [a1]
841 * `--option ' a1 '` gives the list [a1]
842 * `--option a1 --option a2` gives the list [a1, a2]
843 * `--option a1,a2` gives the list [a1, a2]
844 * `--option 'a1, a2'` gives the list [a1, a2]
845
846 @param option The option to be tested.
847 @param getlist A function to return the option's value from
848 parsed command line arguments.
849
850 """
851 a1 = 'mumble@mumbler.com'
852 a2 = 'bumble@bumbler.org'
853 arguments = self._parse_arguments([option, a1])
854 self.assertEqual(getlist(arguments), [a1])
855 arguments = self._parse_arguments([option, ' ' + a1 + ' '])
856 self.assertEqual(getlist(arguments), [a1])
857 arguments = self._parse_arguments([option, a1, option, a2])
858 self.assertEqual(getlist(arguments), [a1, a2])
859 arguments = self._parse_arguments(
860 [option, ','.join([a1, a2])])
861 self.assertEqual(getlist(arguments), [a1, a2])
862 arguments = self._parse_arguments(
863 [option, ', '.join([a1, a2])])
864 self.assertEqual(getlist(arguments), [a1, a2])
865
866
867 def test_board_notify(self):
868 """Test parsing of the `--board-notify` option."""
869 self._check_email_option('--board-notify',
870 lambda a: a.board_notify)
871
872
873 def test_pool_notify(self):
874 """Test parsing of the `--pool-notify` option."""
875 self._check_email_option('--pool-notify',
876 lambda a: a.pool_notify)
877
878
879 def test_pool_logdir(self):
880 """Test parsing of the `--logdir` option."""
881 logdir = '/usr/local/whatsis/logs'
882 arguments = self._parse_arguments(['--logdir', logdir])
883 self.assertEqual(arguments.logdir, logdir)
884
885
886if __name__ == '__main__':
J. Richard Barnettef60a1ee2015-06-02 10:52:37 -0700887 # Some of the functions we test log messages. Prevent those
888 # messages from showing up in test output.
889 logging.getLogger().setLevel(logging.CRITICAL)
J. Richard Barnette96db3492015-03-27 17:23:52 -0700890 unittest.main()