Autotest: Report idle devices for the Lab DUT inventory.
Idle DUTs that are typically locked or wedged, are currently classfied as the
same non-working DUT as broken DUTs.
This fixes two things:
* For the board inventory, request repair for those `BROKEN` DUTs that have
actually failed repair. Add a column that reports on idle DUTs to
the message that is sent to englab-sys-cros.
* For the pool inventory (sent to the deputies) add a column that reports
on idle DUTs, and a detailed idle DUT list. Ask for deputy's attention.
BUG=chromium:590386
TEST=ran lab_inventory_unittest.py locally with --debug, add idle duts
in the test.
Change-Id: I564dd79de69092276aabca6b6714dc175e37dfda
Reviewed-on: https://chromium-review.googlesource.com/332191
Commit-Ready: Xixuan Wu <xixuan@chromium.org>
Tested-by: Xixuan Wu <xixuan@chromium.org>
Reviewed-by: Xixuan Wu <xixuan@chromium.org>
diff --git a/site_utils/lab_inventory.py b/site_utils/lab_inventory.py
index ba34fe0..cddb489 100755
--- a/site_utils/lab_inventory.py
+++ b/site_utils/lab_inventory.py
@@ -141,16 +141,18 @@
* `get_working_list()`
* `get_broken()`
* `get_broken_list()`
+ * `get_idle()`
+ * `get_idle_list()`
The first time any one of these methods is called, it causes
multiple RPC calls with a relatively expensive set of database
queries. However, the results of the queries are cached in the
individual `HostJobHistory` objects, so only the first call
actually pays the full cost.
- Additionally, `get_working_list()` and `get_broken_list()` both
- cache their return values to avoid recalculating lists at every
- call; this caching is separate from the caching of RPC results
- described above.
+ Additionally, `get_working_list()`, `get_broken_list()` and
+ `get_idle_list()` cache their return values to avoid recalculating
+ lists at every call; this caching is separate from the caching of RPC
+ results described above.
This class is deliberately constructed to delay the RPC cost
until the accessor methods are called (rather than to query in
@@ -164,6 +166,7 @@
self._histories = []
self._working_list = None
self._broken_list = None
+ self._idle_list = None
def record_host(self, host_history):
@@ -175,6 +178,7 @@
"""
self._working_list = None
self._broken_list = None
+ self._idle_list = None
self._histories.append(host_history)
@@ -204,7 +208,7 @@
"""Return a list of all broken DUTs in the pool.
Filter `self._histories` for histories where the last
- diagnosis is not `WORKING`.
+ diagnosis is `BROKEN`.
Cache the result so that we only cacluate it once.
@@ -213,7 +217,7 @@
"""
if self._broken_list is None:
self._broken_list = [h for h in self._histories
- if h.last_diagnosis()[0] != status_history.WORKING]
+ if h.last_diagnosis()[0] == status_history.BROKEN]
return self._broken_list
@@ -222,6 +226,29 @@
return len(self.get_broken_list())
+ def get_idle_list(self):
+ """Return a list of all idle DUTs in the pool.
+
+ Filter `self._histories` for histories where the last
+ diagnosis is `UNUSED` or `UNKNOWN`.
+
+ Cache the result so that we only cacluate it once.
+
+ @return A list of HostJobHistory objects.
+
+ """
+ idle_list = [status_history.UNUSED, status_history.UNKNOWN]
+ if self._idle_list is None:
+ self._idle_list = [h for h in self._histories
+ if h.last_diagnosis()[0] in idle_list]
+ return self._idle_list
+
+
+ def get_idle(self):
+ """Return the number of idle DUTs in the pool."""
+ return len(self.get_idle_list())
+
+
def get_total(self):
"""Return the total number of DUTs in the pool."""
return len(self._histories)
@@ -311,8 +338,7 @@
"""Return a list of all broken DUTs for the board.
Go through all HostJobHistory objects in the board's pools,
- selecting the ones where the last diagnosis is not
- `WORKING`.
+ selecting the ones where the last diagnosis is `BROKEN`.
@return A list of HostJobHistory objects.
@@ -334,6 +360,38 @@
return self._count_pool(_PoolCounts.get_broken, pool)
+ def get_idle_list(self, pool=None):
+ """Return a list of all idle DUTs for the board.
+
+ Go through all HostJobHistory objects in the board's pools,
+ selecting the ones where the last diagnosis is `UNUSED` or `UNKNOWN`.
+
+ @param pool: The pool to be counted. If `None`, return the total list
+ across all pools.
+
+ @return A list of HostJobHistory objects.
+
+ """
+ if pool is None:
+ l = []
+ for p in self._pools.values():
+ l.extend(p.get_idle_list())
+ return l
+ else:
+ return _PoolCounts.get_idle_list(self._pools[pool])
+
+
+ def get_idle(self, pool=None):
+ """Return the number of idle DUTs in a pool.
+
+ @param pool: The pool to be counted. If `None`, return the total
+ across all pools.
+
+ @return The total number of idle DUTs in the selected pool(s).
+ """
+ return self._count_pool(_PoolCounts.get_idle, pool)
+
+
def get_spares_buffer(self):
"""Return the the nominal number of working spares.
@@ -664,6 +722,7 @@
logging.debug('Creating board inventory')
nworking = 0
nbroken = 0
+ nidle = 0
nbroken_boards = 0
ntotal_boards = 0
summaries = []
@@ -672,11 +731,12 @@
counts = inventory[board]
# Summary elements laid out in the same order as the text
# headers:
- # Board Avail Bad Good Spare Total
- # e[0] e[1] e[2] e[3] e[4] e[5]
+ # Board Avail Bad Idle Good Spare Total
+ # e[0] e[1] e[2] e[3] e[4] e[5] e[6]
element = (board,
counts.get_spares_buffer(),
counts.get_broken(),
+ counts.get_idle(),
counts.get_working(),
counts.get_total(_SPARE_POOL),
counts.get_total())
@@ -685,15 +745,18 @@
nbroken_boards += 1
ntotal_boards += 1
nbroken += element[2]
- nworking += element[3]
- ntotal = nworking + nbroken
+ nidle += element[3]
+ nworking += element[4]
+ ntotal = nworking + nbroken + nidle
summaries = sorted(summaries, key=lambda e: (e[1], -e[2]))
broken_percent = int(round(100.0 * nbroken / ntotal))
- working_percent = 100 - broken_percent
+ idle_percent = int(round(100.0 * nidle / ntotal))
+ working_percent = 100 - broken_percent - idle_percent
message = ['Summary of DUTs in inventory:',
- '%10s %10s %6s' % ('Bad', 'Good', 'Total'),
- '%5d %3d%% %5d %3d%% %6d' % (
+ '%10s %10s %10s %6s' % ('Bad', 'Idle', 'Good', 'Total'),
+ '%5d %3d%% %5d %3d%% %5d %3d%% %6d' % (
nbroken, broken_percent,
+ nidle, idle_percent,
nworking, working_percent,
ntotal),
'',
@@ -701,11 +764,11 @@
'Boards in inventory: %d' % ntotal_boards,
'', '',
'Full board inventory:\n',
- '%-22s %5s %5s %5s %5s %5s' % (
- 'Board', 'Avail', 'Bad', 'Good',
+ '%-22s %5s %5s %5s %5s %5s %5s' % (
+ 'Board', 'Avail', 'Bad', 'Idle', 'Good',
'Spare', 'Total')]
message.extend(
- ['%-22s %5d %5d %5d %5d %5d' % e for e in summaries])
+ ['%-22s %5d %5d %5d %5d %5d %5d' % e for e in summaries])
return '\n'.join(message)
@@ -741,28 +804,69 @@
message.append(
'%sStatus for pool:%s, by board:' % (newline, pool))
message.append(
- '%-20s %5s %5s %5s' % (
- 'Board', 'Bad', 'Good', 'Total'))
+ '%-20s %5s %5s %5s %5s' % (
+ 'Board', 'Bad', 'Idle', 'Good', 'Total'))
data_list = []
for board, counts in inventory.items():
logging.debug('Counting inventory for %s, %s',
board, pool)
broken = counts.get_broken(pool)
- if broken == 0:
+ idle = counts.get_idle(pool)
+ # boards at full strength are not reported
+ if broken == 0 and idle == 0:
continue
working = counts.get_working(pool)
total = counts.get_total(pool)
- data_list.append((board, broken, working, total))
+ data_list.append((board, broken, idle, working, total))
if data_list:
data_list = sorted(data_list, key=lambda d: -d[1])
message.extend(
- ['%-20s %5d %5d %5d' % t for t in data_list])
+ ['%-20s %5d %5d %5d %5d' % t for t in data_list])
else:
message.append('(All boards at full strength)')
newline = '\n'
return '\n'.join(message)
+_IDLE_INVENTORY_HEADER = '''\
+Notice to Infrastructure deputies: The hosts shown below haven't
+run any jobs for at least 24 hours. Please check each host; locked
+hosts should normally be unlocked; stuck jobs should normally be
+aborted.
+'''
+
+
+def _generate_idle_inventory_message(inventory):
+ """Generate the "idle inventory" e-mail message.
+
+ The idle inventory is a host list with corresponding pool and board,
+ where the hosts are idle (`UNKWOWN` or `UNUSED`).
+
+ N.B. For sample output text format as users can expect to
+ see it in e-mail and log files, refer to the unit tests.
+
+ @param inventory _LabInventory object with the inventory to
+ be reported on.
+ @return String with the inventory message to be sent.
+
+ """
+ logging.debug('Creating idle inventory')
+ message = [_IDLE_INVENTORY_HEADER]
+ message.append('Idle Host List:')
+ message.append('%-30s %-20s %s' % ('Hostname', 'Board', 'Pool'))
+ data_list = []
+ for pool in _MANAGED_POOLS:
+ for board, counts in inventory.items():
+ logging.debug('Counting inventory for %s, %s', board, pool)
+ data_list.extend([(dut.host.hostname, board, pool)
+ for dut in counts.get_idle_list(pool)])
+ if data_list:
+ message.extend(['%-30s %-20s %s' % t for t in data_list])
+ else:
+ message.append('(No idle DUTs)')
+ return '\n'.join(message)
+
+
def _send_email(arguments, tag, subject, recipients, body):
"""Send an inventory e-mail message.
@@ -1036,11 +1140,13 @@
recommend_message + board_message)
if arguments.pool_notify:
+ pool_message = _generate_pool_inventory_message(inventory)
+ idle_message = _generate_idle_inventory_message(inventory)
_send_email(arguments,
'pools-%s.txt' % timestamp,
'DUT pool inventory %s' % timestamp,
arguments.pool_notify,
- _generate_pool_inventory_message(inventory))
+ pool_message + '\n\n\n' + idle_message)
except KeyboardInterrupt:
pass
except EnvironmentError as e:
diff --git a/site_utils/lab_inventory_unittest.py b/site_utils/lab_inventory_unittest.py
index cde911c..04fa980 100755
--- a/site_utils/lab_inventory_unittest.py
+++ b/site_utils/lab_inventory_unittest.py
@@ -13,13 +13,27 @@
from autotest_lib.site_utils import status_history
+class _FakeHost(object):
+ """Class to mock `Host` in _FakeHostHistory for testing."""
+
+ def __init__(self, hostname):
+ self.hostname = hostname
+
+
class _FakeHostHistory(object):
"""Class to mock `HostJobHistory` for testing."""
- def __init__(self, board, pool, status):
+ def __init__(self, board, pool, status, hostname=''):
self._board = board
self._pool = pool
self._status = status
+ self._host = _FakeHost(hostname)
+
+
+ @property
+ def host(self):
+ """Return the recorded host."""
+ return self._host
@property
@@ -73,7 +87,9 @@
]
_WORKING = status_history.WORKING
-_BROKEN = _NON_WORKING_STATUS_LIST[0]
+_UNUSED = _NON_WORKING_STATUS_LIST[0]
+_BROKEN = _NON_WORKING_STATUS_LIST[1]
+_UNKNOWN = _NON_WORKING_STATUS_LIST[2]
class PoolCountTests(unittest.TestCase):
@@ -99,7 +115,7 @@
self._pool_counts.record_host(fake)
- def _check_counts(self, working, broken):
+ def _check_counts(self, working, broken, idle):
"""Check that pool counts match expectations.
Checks that `get_working()` and `get_broken()` return the
@@ -112,38 +128,44 @@
"""
self.assertEqual(self._pool_counts.get_working(), working)
self.assertEqual(self._pool_counts.get_broken(), broken)
+ self.assertEqual(self._pool_counts.get_idle(), idle)
self.assertEqual(self._pool_counts.get_total(),
- working + broken)
+ working + broken + idle)
def test_empty(self):
"""Test counts when there are no DUTs recorded."""
- self._check_counts(0, 0)
+ self._check_counts(0, 0, 0)
- def test_non_working(self):
- """Test counting for all non-working status values."""
- count = 0
- for status in _NON_WORKING_STATUS_LIST:
- self._add_host(status)
- count += 1
- self._check_counts(0, count)
+ def test_broken(self):
+ """Test counting for status: BROKEN."""
+ self._add_host(_BROKEN)
+ self._check_counts(0, 1, 0)
+
+
+ def test_idle(self):
+ """Testing counting for idle status values."""
+ self._add_host(_UNUSED)
+ self._check_counts(0, 0, 1)
+ self._add_host(_UNKNOWN)
+ self._check_counts(0, 0, 2)
def test_working_then_broken(self):
"""Test counts after adding a working and then a broken DUT."""
self._add_host(_WORKING)
- self._check_counts(1, 0)
+ self._check_counts(1, 0, 0)
self._add_host(_BROKEN)
- self._check_counts(1, 1)
+ self._check_counts(1, 1, 0)
def test_broken_then_working(self):
"""Test counts after adding a broken and then a working DUT."""
self._add_host(_BROKEN)
- self._check_counts(0, 1)
+ self._check_counts(0, 1, 0)
self._add_host(_WORKING)
- self._check_counts(1, 1)
+ self._check_counts(1, 1, 0)
class BoardCountTests(unittest.TestCase):
@@ -474,7 +496,7 @@
"""
histories = []
self.num_duts = 0
- status_choices = (_WORKING, _BROKEN)
+ status_choices = (_WORKING, _BROKEN, _UNUSED)
pools = (self._CRITICAL_POOL, self._SPARE_POOL)
for board, counts in data.items():
for i in range(0, len(pools)):
@@ -533,23 +555,29 @@
"""
working_total = 0
broken_total = 0
+ idle_total = 0
managed_boards = set()
for b in self.inventory:
c = self.inventory[b]
calculated_counts = (
(c.get_working(self._CRITICAL_POOL),
- c.get_broken(self._CRITICAL_POOL)),
+ c.get_broken(self._CRITICAL_POOL),
+ c.get_idle(self._CRITICAL_POOL)),
(c.get_working(self._SPARE_POOL),
- c.get_broken(self._SPARE_POOL)))
+ c.get_broken(self._SPARE_POOL),
+ c.get_idle(self._SPARE_POOL)))
self.assertEqual(data[b], calculated_counts)
nworking = data[b][0][0] + data[b][1][0]
nbroken = data[b][0][1] + data[b][1][1]
+ nidle = data[b][0][2] + data[b][1][2]
self.assertEqual(nworking, len(c.get_working_list()))
self.assertEqual(nbroken, len(c.get_broken_list()))
+ self.assertEqual(nidle, len(c.get_idle_list()))
working_total += nworking
broken_total += nbroken
- ncritical = data[b][0][0] + data[b][0][1]
- nspare = data[b][1][0] + data[b][1][1]
+ idle_total += nidle
+ ncritical = data[b][0][0] + data[b][0][1] + data[b][0][2]
+ nspare = data[b][1][0] + data[b][1][1] + data[b][1][2]
if ncritical != 0 and nspare != 0:
managed_boards.add(b)
self.assertEqual(self.inventory.get_managed_boards(),
@@ -570,14 +598,14 @@
def test_missing_board(self):
"""Test handling when the board is `None`."""
- self.create_inventory({None: ((1, 1), (1, 1))})
+ self.create_inventory({None: ((1, 1, 1), (1, 1, 1))})
self._check_inventory({})
def test_board_counts(self):
"""Test counts for various numbers of boards."""
for nboards in [1, 2, len(self._BOARD_LIST)]:
- counts = ((1, 1), (1, 1))
+ counts = ((1, 1, 1), (1, 1, 1))
slice = self._BOARD_LIST[0 : nboards]
inventory_data = {
board: counts for board in slice
@@ -589,10 +617,12 @@
def test_single_dut_counts(self):
"""Test counts when there is a single DUT per board."""
testcounts = [
- ((1, 0), (0, 0)),
- ((0, 1), (0, 0)),
- ((0, 0), (1, 0)),
- ((0, 0), (0, 1)),
+ ((1, 0, 0), (0, 0, 0)),
+ ((0, 1, 0), (0, 0, 0)),
+ ((0, 0, 0), (1, 0, 0)),
+ ((0, 0, 0), (0, 1, 0)),
+ ((0, 0, 1), (0, 0, 0)),
+ ((0, 0, 0), (0, 0, 1)),
]
for counts in testcounts:
inventory_data = { self._BOARD_LIST[0]: counts }
@@ -617,12 +647,12 @@
# that simply parrot the original output generation code.
_BOARD_MESSAGE_TEMPLATE = '''
-Board Avail Bad Good Spare Total
-lion -1 13 11 12 24
-tiger -1 5 9 4 14
-bear 0 7 10 7 17
-aardvark 1 6 6 7 12
-platypus 2 4 20 6 24
+Board Avail Bad Idle Good Spare Total
+lion -1 13 2 11 12 26
+tiger -1 5 2 9 4 16
+bear 0 5 2 10 5 17
+platypus 4 2 2 20 6 24
+aardvark 7 2 2 6 9 10
'''
@@ -649,30 +679,36 @@
for l in self._board_lines:
items = l.split()
board = items[0]
- good = int(items[3])
bad = int(items[2])
- spare = int(items[4])
- self._board_data.append((board, (good, bad, spare)))
+ idle = int(items[3])
+ good = int(items[4])
+ spare = int(items[5])
+ self._board_data.append((board, (good, bad, idle, spare)))
def _make_minimum_spares(self, counts):
"""Create a counts tuple with as few spare DUTs as possible."""
- good, bad, spares = counts
- if spares > bad:
- return ((good + bad - spares, 0),
- (spares - bad, bad))
+ good, bad, idle, spares = counts
+ if spares > bad + idle:
+ return ((good + bad +idle - spares, 0, 0),
+ (spares - bad - idle, bad, idle))
+ elif spares < bad:
+ return ((good, bad - spares, idle), (0, spares, 0))
else:
- return ((good, bad - spares), (0, spares))
+ return ((good, 0, idle + bad - spares), (0, bad, spares - bad))
def _make_maximum_spares(self, counts):
"""Create a counts tuple with as many spare DUTs as possible."""
- good, bad, spares = counts
+ good, bad, idle, spares = counts
if good > spares:
- return ((good - spares, bad), (spares, 0))
+ return ((good - spares, bad, idle), (spares, 0, 0))
+ elif good + bad > spares:
+ return ((0, good + bad - spares, idle),
+ (good, spares - good, 0))
else:
- return ((0, good + bad - spares),
- (good, spares - good))
+ return ((0, 0, good + bad + idle - spares),
+ (good, bad, spares - good - bad))
def _check_board_inventory(self, data):
@@ -721,7 +757,7 @@
board: self._make_maximum_spares(counts)
for board, counts in self._board_data
}
- data['elephant'] = ((5, 4), (0, 0))
+ data['elephant'] = ((5, 4, 0), (0, 0, 0))
self._check_board_inventory(data)
@@ -731,7 +767,7 @@
board: self._make_maximum_spares(counts)
for board, counts in self._board_data
}
- data['elephant'] = ((0, 0), (1, 5))
+ data['elephant'] = ((0, 0, 0), (1, 5, 1))
self._check_board_inventory(data)
@@ -741,10 +777,53 @@
board: self._make_maximum_spares(counts)
for board, counts in self._board_data
}
- data['elephant'] = ((5, 0), (5, 0))
+ data['elephant'] = ((5, 0, 1), (5, 0, 1))
self._check_board_inventory(data)
+class _PoolInventoryTestBase(unittest.TestCase):
+ """Parent class for tests relating to generating pool inventory messages.
+
+ Func `setUp` in the class parses a given |message_template| to obtain
+ header and body.
+ """
+ def _read_template(self, message_template):
+ """Read message template for PoolInventoryTest and IdleInventoryTest.
+
+ @param message_template: the input template to be parsed into: header
+ and content (report_lines).
+
+ """
+ message_lines = message_template.split('\n')
+ self._header = message_lines[1]
+ self._report_lines = message_lines[2:-1]
+
+
+ def _check_report_no_info(self, text):
+ """Test a message body containing no reported info.
+
+ The input `text` was created from a query to an inventory, which has
+ no objects meet the query and leads to an `empty` return. Assert that
+ the text consists of a single line starting with '(' and ending with ')'.
+
+ @param text: Message body text to be tested.
+
+ """
+ self.assertTrue(len(text) == 1 and
+ text[0][0] == '(' and
+ text[0][-1] == ')')
+
+
+ def _check_report(self, text):
+ """Test a message against the passed |expected_content|.
+
+ @param text: Message body text to be tested.
+ @param expected_content: The ground-truth content to be compared with.
+
+ """
+ self.assertEqual(text, self._report_lines)
+
+
# _POOL_MESSAGE_TEMPLATE -
# This is a sample of the output text produced by
# _generate_pool_inventory_message(). This string is parsed by the
@@ -756,19 +835,18 @@
# rationale on using sample text in this way.
_POOL_MESSAGE_TEMPLATE = '''
-Board Bad Good Total
-lion 5 6 11
-tiger 4 5 9
-bear 3 7 10
-aardvark 2 0 2
-platypus 1 1 2
+Board Bad Idle Good Total
+lion 5 2 6 13
+tiger 4 1 5 10
+bear 3 0 7 10
+aardvark 2 0 0 2
+platypus 1 1 1 3
'''
_POOL_ADMIN_URL = 'http://go/cros-manage-duts'
-
-class PoolInventoryTests(unittest.TestCase):
+class PoolInventoryTests(_PoolInventoryTestBase):
"""Tests for `_generate_pool_inventory_message()`.
The tests create various test inventories designed to match the
@@ -801,19 +879,16 @@
the `'\n'` separator.
"""
-
def setUp(self):
- message_lines = _POOL_MESSAGE_TEMPLATE.split('\n')
- self._header = message_lines[1]
- self._board_lines = message_lines[2:-1]
+ super(PoolInventoryTests, self)._read_template(_POOL_MESSAGE_TEMPLATE)
self._board_data = []
- for l in self._board_lines:
+ for l in self._report_lines:
items = l.split()
board = items[0]
- good = int(items[2])
bad = int(items[1])
- self._board_data.append((board, (good, bad)))
- self._inventory = None
+ idle = int(items[2])
+ good = int(items[3])
+ self._board_data.append((board, (good, bad, idle)))
def _create_histories(self, pools, board_data):
@@ -838,7 +913,7 @@
"""
histories = []
- status_choices = (_WORKING, _BROKEN)
+ status_choices = (_WORKING, _BROKEN, _UNUSED)
for pool in pools:
for board, counts in board_data:
for status, count in zip(status_choices, counts):
@@ -870,9 +945,9 @@
(a list of lines) for the board.
"""
- self._inventory = lab_inventory._LabInventory(histories)
+ inventory = lab_inventory._LabInventory(histories)
message = lab_inventory._generate_pool_inventory_message(
- self._inventory).split('\n')
+ inventory).split('\n')
poolset = set(lab_inventory._CRITICAL_POOLS)
seen_url = False
seen_intro = False
@@ -906,38 +981,11 @@
return board_text
- def _check_inventory_no_shortages(self, text):
- """Test a message body containing no reported shortages.
-
- The input `text` was created for a pool containing no
- board shortages. Assert that the text consists of a
- single line starting with '(' and ending with ')'.
-
- @param text Message body text to be tested.
-
- """
- self.assertTrue(len(text) == 1 and
- text[0][0] == '(' and
- text[0][-1] == ')')
-
-
- def _check_inventory(self, text):
- """Test a message against `_POOL_MESSAGE_TEMPLATE`.
-
- Test that the given message text matches the parsed
- `_POOL_MESSAGE_TEMPLATE`.
-
- @param text Message body text to be tested.
-
- """
- self.assertEqual(text, self._board_lines)
-
-
def test_no_shortages(self):
"""Test correct output when no pools have shortages."""
board_text = self._parse_pool_summaries([])
for text in board_text.values():
- self._check_inventory_no_shortages(text)
+ self._check_report_no_info(text)
def test_one_pool_shortage(self):
@@ -949,9 +997,9 @@
for checkpool in lab_inventory._CRITICAL_POOLS:
text = board_text[checkpool]
if checkpool == pool:
- self._check_inventory(text)
+ self._check_report(text)
else:
- self._check_inventory_no_shortages(text)
+ self._check_report_no_info(text)
def test_all_pool_shortages(self):
@@ -963,32 +1011,115 @@
self._board_data))
board_text = self._parse_pool_summaries(histories)
for pool in lab_inventory._CRITICAL_POOLS:
- self._check_inventory(board_text[pool])
+ self._check_report(board_text[pool])
def test_full_board_ignored(self):
"""Test that boards at full strength are not reported."""
pool = lab_inventory._CRITICAL_POOLS[0]
- full_board = [('echidna', (5, 0))]
+ full_board = [('echidna', (5, 0, 0))]
histories = self._create_histories((pool,),
full_board)
text = self._parse_pool_summaries(histories)[pool]
- self._check_inventory_no_shortages(text)
+ self._check_report_no_info(text)
board_data = self._board_data + full_board
histories = self._create_histories((pool,), board_data)
text = self._parse_pool_summaries(histories)[pool]
- self._check_inventory(text)
+ self._check_report(text)
def test_spare_pool_ignored(self):
"""Test that reporting ignores the spare pool inventory."""
spare_pool = lab_inventory._SPARE_POOL
- spare_data = self._board_data + [('echidna', (0, 5))]
+ spare_data = self._board_data + [('echidna', (0, 5, 0))]
histories = self._create_histories((spare_pool,),
spare_data)
board_text = self._parse_pool_summaries(histories)
for pool in lab_inventory._CRITICAL_POOLS:
- self._check_inventory_no_shortages(board_text[pool])
+ self._check_report_no_info(board_text[pool])
+
+
+_IDLE_MESSAGE_TEMPLATE = '''
+Hostname Board Pool
+chromeos4-row12-rack4-host7 tiger bvt
+chromeos1-row3-rack1-host2 lion bvt
+chromeos3-row2-rack2-host5 lion cq
+chromeos2-row7-rack3-host11 platypus suites
+'''
+
+
+class IdleInventoryTests(_PoolInventoryTestBase):
+ """Tests for `_generate_idle_inventory_message()`.
+
+ The tests create idle duts that match the counts and pool in
+ `_IDLE_MESSAGE_TEMPLATE`. In test, it asserts that the generated
+ idle message text matches the format established in
+ `_IDLE_MESSAGE_TEMPLATE`.
+
+ Parse message text is represented as a list of strings, split on
+ the `'\n'` separator.
+
+ """
+
+ def setUp(self):
+ super(IdleInventoryTests, self)._read_template(_IDLE_MESSAGE_TEMPLATE)
+ self._host_data = []
+ for h in self._report_lines:
+ items = h.split()
+ hostname = items[0]
+ board = items[1]
+ pool = items[2]
+ self._host_data.append((hostname, board, pool))
+ self._histories = []
+ self._histories.append(_FakeHostHistory('echidna', 'bvt', _BROKEN))
+ self._histories.append(_FakeHostHistory('lion', 'bvt', _WORKING))
+
+
+ def _add_idles(self):
+ """Add idle duts from `_IDLE_MESSAGE_TEMPLATE`."""
+ idle_histories = [_FakeHostHistory(
+ board, pool, _UNUSED, hostname=hostname)
+ for hostname, board, pool in self._host_data]
+ self._histories.extend(idle_histories)
+
+
+ def _check_header(self, text):
+ """Check whether header in the template `_IDLE_MESSAGE_TEMPLATE` is in
+ passed text."""
+ self.assertIn(self._header, text)
+
+
+ def _get_idle_message(self, histories):
+ """Generate idle inventory and obtain its message.
+
+ @param histories: Used to create lab inventory.
+
+ @return the generated idle message.
+
+ """
+ inventory = lab_inventory._LabInventory(histories)
+ message = lab_inventory._generate_idle_inventory_message(
+ inventory).split('\n')
+ return message
+
+
+ def test_check_idle_inventory(self):
+ """Test that reporting all the idle DUTs for every pool, sorted by
+ lab_inventory._MANAGED_POOLS.
+ """
+ self._add_idles()
+
+ message = self._get_idle_message(self._histories)
+ self._check_header(message)
+ self._check_report(message[message.index(self._header) + 1 :])
+
+
+ def test_no_idle_inventory(self):
+ """Test that reporting no idle DUTs."""
+ message = self._get_idle_message(self._histories)
+ self._check_header(message)
+ self._check_report_no_info(
+ message[message.index(self._header) + 1 :])
class CommandParsingTests(unittest.TestCase):