391 lines
13 KiB
Python
391 lines
13 KiB
Python
|
# Lint as: python2, python3
|
||
|
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
|
||
|
# Use of this source code is governed by a BSD-style license that can be
|
||
|
# found in the LICENSE file.
|
||
|
|
||
|
from __future__ import absolute_import
|
||
|
from __future__ import division
|
||
|
from __future__ import print_function
|
||
|
|
||
|
from six.moves import range
|
||
|
import six
|
||
|
import json
|
||
|
import mox
|
||
|
import time
|
||
|
import unittest
|
||
|
from six.moves import urllib
|
||
|
|
||
|
import common
|
||
|
from autotest_lib.client.common_lib import global_config
|
||
|
from autotest_lib.server import site_utils
|
||
|
|
||
|
_DEADBUILD = 'deadboard-release/R33-4966.0.0'
|
||
|
_LIVEBUILD = 'liveboard-release/R32-4920.14.0'
|
||
|
|
||
|
_STATUS_TEMPLATE = '''
|
||
|
{
|
||
|
"username": "fizzbin@google.com",
|
||
|
"date": "2013-11-16 00:25:23.511208",
|
||
|
"message": "%s",
|
||
|
"can_commit_freely": %s,
|
||
|
"general_state": "%s"
|
||
|
}
|
||
|
'''
|
||
|
|
||
|
|
||
|
def _make_status(message, can_commit, state):
|
||
|
return _STATUS_TEMPLATE % (message, can_commit, state)
|
||
|
|
||
|
|
||
|
def _make_open_status(message, state):
|
||
|
return _make_status(message, 'true', state)
|
||
|
|
||
|
|
||
|
def _make_closed_status(message):
|
||
|
return _make_status(message, 'false', 'closed')
|
||
|
|
||
|
|
||
|
def _make_deadbuild_status(message):
|
||
|
return _make_status(message, 'false', 'open')
|
||
|
|
||
|
|
||
|
_OPEN_STATUS_VALUES = [
|
||
|
_make_open_status('Lab is up (cross your fingers)', 'open'),
|
||
|
_make_open_status('Lab is on fire', 'throttled'),
|
||
|
_make_open_status('Lab is up despite deadboard', 'open'),
|
||
|
_make_open_status('Lab is up despite .*/R33-4966.0.0', 'open'),
|
||
|
]
|
||
|
|
||
|
_CLOSED_STATUS_VALUES = [
|
||
|
_make_closed_status('Lab is down for spite'),
|
||
|
_make_closed_status('Lab is down even for [%s]' % _LIVEBUILD),
|
||
|
_make_closed_status('Lab is down even for [%s]' % _DEADBUILD),
|
||
|
]
|
||
|
|
||
|
_DEADBUILD_STATUS_VALUES = [
|
||
|
_make_deadbuild_status('Lab is up except for [deadboard-]'),
|
||
|
_make_deadbuild_status('Lab is up except for [board- deadboard-]'),
|
||
|
_make_deadbuild_status('Lab is up except for [.*/R33-]'),
|
||
|
_make_deadbuild_status('Lab is up except for [deadboard-.*/R33-]'),
|
||
|
_make_deadbuild_status('Lab is up except for [ deadboard-]'),
|
||
|
_make_deadbuild_status('Lab is up except for [deadboard- ]'),
|
||
|
_make_deadbuild_status('Lab is up [first .*/R33- last]'),
|
||
|
_make_deadbuild_status('liveboard is good, but [deadboard-] is bad'),
|
||
|
_make_deadbuild_status('Lab is up [deadboard- otherboard-]'),
|
||
|
_make_deadbuild_status('Lab is up [otherboard- deadboard-]'),
|
||
|
]
|
||
|
|
||
|
|
||
|
_FAKE_URL = 'ignore://not.a.url'
|
||
|
|
||
|
|
||
|
class _FakeURLResponse(object):
|
||
|
|
||
|
"""Everything needed to pretend to be a response from urlopen().
|
||
|
|
||
|
Creates a StringIO instance to handle the File operations.
|
||
|
|
||
|
N.B. StringIO is lame: we can't inherit from it (super won't
|
||
|
work), and it doesn't implement __getattr__(), either. So, we
|
||
|
have to manually forward calls to the StringIO object. This
|
||
|
forwards only what empirical testing says is required; YMMV.
|
||
|
|
||
|
"""
|
||
|
|
||
|
def __init__(self, code, buffer):
|
||
|
self._stringio = six.StringIO(buffer)
|
||
|
self._code = code
|
||
|
|
||
|
|
||
|
def read(self, size=-1):
|
||
|
"""Standard file-like read operation.
|
||
|
|
||
|
@param size size for read operation.
|
||
|
"""
|
||
|
return self._stringio.read(size)
|
||
|
|
||
|
|
||
|
def getcode(self):
|
||
|
"""Get URL HTTP response code."""
|
||
|
return self._code
|
||
|
|
||
|
|
||
|
class GetStatusTest(mox.MoxTestBase):
|
||
|
|
||
|
"""Test case for _get_lab_status().
|
||
|
|
||
|
We mock out dependencies on urllib2 and time.sleep(), and
|
||
|
confirm that the function returns the proper JSON representation
|
||
|
for a pre-defined response.
|
||
|
|
||
|
"""
|
||
|
|
||
|
def setUp(self):
|
||
|
super(GetStatusTest, self).setUp()
|
||
|
self.mox.StubOutWithMock(urllib.request, 'urlopen')
|
||
|
self.mox.StubOutWithMock(time, 'sleep')
|
||
|
|
||
|
|
||
|
def test_success(self):
|
||
|
"""Test that successful calls to urlopen() succeed."""
|
||
|
json_string = _OPEN_STATUS_VALUES[0]
|
||
|
json_value = json.loads(json_string)
|
||
|
urllib.request.urlopen(mox.IgnoreArg()).AndReturn(
|
||
|
_FakeURLResponse(200, json_string))
|
||
|
self.mox.ReplayAll()
|
||
|
result = site_utils._get_lab_status(_FAKE_URL)
|
||
|
self.mox.VerifyAll()
|
||
|
self.assertEqual(json_value, result)
|
||
|
|
||
|
|
||
|
def test_retry_ioerror(self):
|
||
|
"""Test that an IOError retries at least once."""
|
||
|
json_string = _OPEN_STATUS_VALUES[0]
|
||
|
json_value = json.loads(json_string)
|
||
|
urllib.request.urlopen(mox.IgnoreArg()).AndRaise(
|
||
|
IOError('Fake I/O error for a fake URL'))
|
||
|
time.sleep(mox.IgnoreArg()).AndReturn(None)
|
||
|
urllib.request.urlopen(mox.IgnoreArg()).AndReturn(
|
||
|
_FakeURLResponse(200, json_string))
|
||
|
self.mox.ReplayAll()
|
||
|
result = site_utils._get_lab_status(_FAKE_URL)
|
||
|
self.mox.VerifyAll()
|
||
|
self.assertEqual(json_value, result)
|
||
|
|
||
|
|
||
|
def test_retry_http_internal_error(self):
|
||
|
"""Test that an HTTP error retries at least once."""
|
||
|
json_string = _OPEN_STATUS_VALUES[0]
|
||
|
json_value = json.loads(json_string)
|
||
|
urllib.request.urlopen(mox.IgnoreArg()).AndReturn(
|
||
|
_FakeURLResponse(500, ''))
|
||
|
time.sleep(mox.IgnoreArg()).AndReturn(None)
|
||
|
urllib.request.urlopen(mox.IgnoreArg()).AndReturn(
|
||
|
_FakeURLResponse(200, json_string))
|
||
|
self.mox.ReplayAll()
|
||
|
result = site_utils._get_lab_status(_FAKE_URL)
|
||
|
self.mox.VerifyAll()
|
||
|
self.assertEqual(json_value, result)
|
||
|
|
||
|
|
||
|
def test_failure_ioerror(self):
|
||
|
"""Test that there's a failure if urlopen() never succeeds."""
|
||
|
json_string = _OPEN_STATUS_VALUES[0]
|
||
|
json_value = json.loads(json_string)
|
||
|
for _ in range(site_utils._MAX_LAB_STATUS_ATTEMPTS):
|
||
|
urllib.request.urlopen(mox.IgnoreArg()).AndRaise(
|
||
|
IOError('Fake I/O error for a fake URL'))
|
||
|
time.sleep(mox.IgnoreArg()).AndReturn(None)
|
||
|
self.mox.ReplayAll()
|
||
|
result = site_utils._get_lab_status(_FAKE_URL)
|
||
|
self.mox.VerifyAll()
|
||
|
self.assertEqual(None, result)
|
||
|
|
||
|
|
||
|
def test_failure_http_internal_error(self):
|
||
|
"""Test that there's a failure for a permanent HTTP error."""
|
||
|
json_string = _OPEN_STATUS_VALUES[0]
|
||
|
json_value = json.loads(json_string)
|
||
|
for _ in range(site_utils._MAX_LAB_STATUS_ATTEMPTS):
|
||
|
urllib.request.urlopen(mox.IgnoreArg()).AndReturn(
|
||
|
_FakeURLResponse(404, 'Not here, never gonna be'))
|
||
|
time.sleep(mox.IgnoreArg()).InAnyOrder().AndReturn(None)
|
||
|
self.mox.ReplayAll()
|
||
|
result = site_utils._get_lab_status(_FAKE_URL)
|
||
|
self.mox.VerifyAll()
|
||
|
self.assertEqual(None, result)
|
||
|
|
||
|
|
||
|
class DecodeStatusTest(unittest.TestCase):
|
||
|
|
||
|
"""Test case for _decode_lab_status().
|
||
|
|
||
|
Testing covers three distinct possible states:
|
||
|
1. Lab is up. All calls to _decode_lab_status() will
|
||
|
succeed without raising an exception.
|
||
|
2. Lab is down. All calls to _decode_lab_status() will
|
||
|
fail with TestLabException.
|
||
|
3. Build disabled. Calls to _decode_lab_status() will
|
||
|
succeed, except that board `_DEADBUILD` will raise
|
||
|
TestLabException.
|
||
|
|
||
|
"""
|
||
|
|
||
|
def _assert_lab_open(self, lab_status):
|
||
|
"""Test that open status values are handled properly.
|
||
|
|
||
|
Test that _decode_lab_status() succeeds when the lab status
|
||
|
is up.
|
||
|
|
||
|
@param lab_status JSON value describing lab status.
|
||
|
|
||
|
"""
|
||
|
site_utils._decode_lab_status(lab_status, _LIVEBUILD)
|
||
|
site_utils._decode_lab_status(lab_status, _DEADBUILD)
|
||
|
|
||
|
|
||
|
def _assert_lab_closed(self, lab_status):
|
||
|
"""Test that closed status values are handled properly.
|
||
|
|
||
|
Test that _decode_lab_status() raises TestLabException
|
||
|
when the lab status is down.
|
||
|
|
||
|
@param lab_status JSON value describing lab status.
|
||
|
|
||
|
"""
|
||
|
with self.assertRaises(site_utils.TestLabException):
|
||
|
site_utils._decode_lab_status(lab_status, _LIVEBUILD)
|
||
|
with self.assertRaises(site_utils.TestLabException):
|
||
|
site_utils._decode_lab_status(lab_status, _DEADBUILD)
|
||
|
|
||
|
|
||
|
def _assert_lab_deadbuild(self, lab_status):
|
||
|
"""Test that disabled builds are handled properly.
|
||
|
|
||
|
Test that _decode_lab_status() raises TestLabException
|
||
|
for build `_DEADBUILD` and succeeds otherwise.
|
||
|
|
||
|
@param lab_status JSON value describing lab status.
|
||
|
|
||
|
"""
|
||
|
site_utils._decode_lab_status(lab_status, _LIVEBUILD)
|
||
|
with self.assertRaises(site_utils.TestLabException):
|
||
|
site_utils._decode_lab_status(lab_status, _DEADBUILD)
|
||
|
|
||
|
|
||
|
def _assert_lab_status(self, test_values, checker):
|
||
|
"""General purpose test for _decode_lab_status().
|
||
|
|
||
|
Decode each JSON string in `test_values`, and call the
|
||
|
`checker` function to test the corresponding status is
|
||
|
correctly handled.
|
||
|
|
||
|
@param test_values Array of JSON encoded strings representing
|
||
|
lab status.
|
||
|
@param checker Function to be called against each of the lab
|
||
|
status values in the `test_values` array.
|
||
|
|
||
|
"""
|
||
|
for s in test_values:
|
||
|
lab_status = json.loads(s)
|
||
|
checker(lab_status)
|
||
|
|
||
|
|
||
|
def test_open_lab(self):
|
||
|
"""Test that open lab status values are handled correctly."""
|
||
|
self._assert_lab_status(_OPEN_STATUS_VALUES,
|
||
|
self._assert_lab_open)
|
||
|
|
||
|
|
||
|
def test_closed_lab(self):
|
||
|
"""Test that closed lab status values are handled correctly."""
|
||
|
self._assert_lab_status(_CLOSED_STATUS_VALUES,
|
||
|
self._assert_lab_closed)
|
||
|
|
||
|
|
||
|
def test_dead_build(self):
|
||
|
"""Test that disabled builds are handled correctly."""
|
||
|
self._assert_lab_status(_DEADBUILD_STATUS_VALUES,
|
||
|
self._assert_lab_deadbuild)
|
||
|
|
||
|
|
||
|
class CheckStatusTest(mox.MoxTestBase):
|
||
|
|
||
|
"""Test case for `check_lab_status()`.
|
||
|
|
||
|
We mock out dependencies on `global_config.global_config()`,
|
||
|
`_get_lab_status()` and confirm that the function succeeds or
|
||
|
fails as expected.
|
||
|
|
||
|
N.B. We don't mock `_decode_lab_status()`; if DecodeStatusTest
|
||
|
is failing, this test may fail, too.
|
||
|
|
||
|
"""
|
||
|
|
||
|
def setUp(self):
|
||
|
super(CheckStatusTest, self).setUp()
|
||
|
self.mox.StubOutWithMock(global_config.global_config,
|
||
|
'get_config_value')
|
||
|
self.mox.StubOutWithMock(site_utils, '_get_lab_status')
|
||
|
|
||
|
|
||
|
def _setup_not_cautotest(self):
|
||
|
"""Set up to mock the "we're not on cautotest" case."""
|
||
|
global_config.global_config.get_config_value(
|
||
|
'SERVER', 'hostname').AndReturn('not-cautotest')
|
||
|
|
||
|
|
||
|
def _setup_no_status(self):
|
||
|
"""Set up to mock lab status as unavailable."""
|
||
|
global_config.global_config.get_config_value(
|
||
|
'SERVER', 'hostname').AndReturn('cautotest')
|
||
|
global_config.global_config.get_config_value(
|
||
|
'CROS', 'lab_status_url').AndReturn(_FAKE_URL)
|
||
|
site_utils._get_lab_status(_FAKE_URL).AndReturn(None)
|
||
|
|
||
|
|
||
|
def _setup_lab_status(self, json_string):
|
||
|
"""Set up to mock a given lab status.
|
||
|
|
||
|
@param json_string JSON string for the JSON object to return
|
||
|
from `_get_lab_status()`.
|
||
|
|
||
|
"""
|
||
|
global_config.global_config.get_config_value(
|
||
|
'SERVER', 'hostname').AndReturn('cautotest')
|
||
|
global_config.global_config.get_config_value(
|
||
|
'CROS', 'lab_status_url').AndReturn(_FAKE_URL)
|
||
|
json_value = json.loads(json_string)
|
||
|
site_utils._get_lab_status(_FAKE_URL).AndReturn(json_value)
|
||
|
|
||
|
|
||
|
def _try_check_status(self, build):
|
||
|
"""Test calling check_lab_status() with `build`."""
|
||
|
try:
|
||
|
self.mox.ReplayAll()
|
||
|
site_utils.check_lab_status(build)
|
||
|
finally:
|
||
|
self.mox.VerifyAll()
|
||
|
|
||
|
|
||
|
def test_non_cautotest(self):
|
||
|
"""Test a call with a build when the host isn't cautotest."""
|
||
|
self._setup_not_cautotest()
|
||
|
self._try_check_status(_LIVEBUILD)
|
||
|
|
||
|
|
||
|
def test_no_lab_status(self):
|
||
|
"""Test with a build when `_get_lab_status()` returns `None`."""
|
||
|
self._setup_no_status()
|
||
|
self._try_check_status(_LIVEBUILD)
|
||
|
|
||
|
|
||
|
def test_lab_up_live_build(self):
|
||
|
"""Test lab open with a build specified."""
|
||
|
self._setup_lab_status(_OPEN_STATUS_VALUES[0])
|
||
|
self._try_check_status(_LIVEBUILD)
|
||
|
|
||
|
|
||
|
def test_lab_down_live_build(self):
|
||
|
"""Test lab closed with a build specified."""
|
||
|
self._setup_lab_status(_CLOSED_STATUS_VALUES[0])
|
||
|
with self.assertRaises(site_utils.TestLabException):
|
||
|
self._try_check_status(_LIVEBUILD)
|
||
|
|
||
|
|
||
|
def test_build_disabled_live_build(self):
|
||
|
"""Test build disabled with a live build specified."""
|
||
|
self._setup_lab_status(_DEADBUILD_STATUS_VALUES[0])
|
||
|
self._try_check_status(_LIVEBUILD)
|
||
|
|
||
|
|
||
|
def test_build_disabled_dead_build(self):
|
||
|
"""Test build disabled with the disabled build specified."""
|
||
|
self._setup_lab_status(_DEADBUILD_STATUS_VALUES[0])
|
||
|
with self.assertRaises(site_utils.TestLabException):
|
||
|
self._try_check_status(_DEADBUILD)
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
unittest.main()
|