1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import StringIO 6import json 7import mox 8import time 9import unittest 10import urllib2 11 12import common 13from autotest_lib.client.common_lib import global_config 14from autotest_lib.server import site_utils 15 16_DEADBUILD = 'deadboard-release/R33-4966.0.0' 17_LIVEBUILD = 'liveboard-release/R32-4920.14.0' 18 19_STATUS_TEMPLATE = ''' 20 { 21 "username": "fizzbin@google.com", 22 "date": "2013-11-16 00:25:23.511208", 23 "message": "%s", 24 "can_commit_freely": %s, 25 "general_state": "%s" 26 } 27 ''' 28 29 30def _make_status(message, can_commit, state): 31 return _STATUS_TEMPLATE % (message, can_commit, state) 32 33 34def _make_open_status(message, state): 35 return _make_status(message, 'true', state) 36 37 38def _make_closed_status(message): 39 return _make_status(message, 'false', 'closed') 40 41 42def _make_deadbuild_status(message): 43 return _make_status(message, 'false', 'open') 44 45 46_OPEN_STATUS_VALUES = [ 47 _make_open_status('Lab is up (cross your fingers)', 'open'), 48 _make_open_status('Lab is on fire', 'throttled'), 49 _make_open_status('Lab is up despite deadboard', 'open'), 50 _make_open_status('Lab is up despite .*/R33-4966.0.0', 'open'), 51] 52 53_CLOSED_STATUS_VALUES = [ 54 _make_closed_status('Lab is down for spite'), 55 _make_closed_status('Lab is down even for [%s]' % _LIVEBUILD), 56 _make_closed_status('Lab is down even for [%s]' % _DEADBUILD), 57] 58 59_DEADBUILD_STATUS_VALUES = [ 60 _make_deadbuild_status('Lab is up except for [deadboard-]'), 61 _make_deadbuild_status('Lab is up except for [board- deadboard-]'), 62 _make_deadbuild_status('Lab is up except for [.*/R33-]'), 63 _make_deadbuild_status('Lab is up except for [deadboard-.*/R33-]'), 64 _make_deadbuild_status('Lab is up except for [ deadboard-]'), 65 _make_deadbuild_status('Lab is up except for [deadboard- ]'), 66 _make_deadbuild_status('Lab is up [first .*/R33- last]'), 67 _make_deadbuild_status('liveboard is good, but [deadboard-] is bad'), 68 _make_deadbuild_status('Lab is up [deadboard- otherboard-]'), 69 _make_deadbuild_status('Lab is up [otherboard- deadboard-]'), 70] 71 72 73_FAKE_URL = 'ignore://not.a.url' 74 75 76class _FakeURLResponse(object): 77 78 """Everything needed to pretend to be a response from urlopen(). 79 80 Creates a StringIO instance to handle the File operations. 81 82 N.B. StringIO is lame: we can't inherit from it (super won't 83 work), and it doesn't implement __getattr__(), either. So, we 84 have to manually forward calls to the StringIO object. This 85 forwards only what empirical testing says is required; YMMV. 86 87 """ 88 89 def __init__(self, code, buffer): 90 self._stringio = StringIO.StringIO(buffer) 91 self._code = code 92 93 94 def read(self, size=-1): 95 """Standard file-like read operation. 96 97 @param size size for read operation. 98 """ 99 return self._stringio.read(size) 100 101 102 def getcode(self): 103 """Get URL HTTP response code.""" 104 return self._code 105 106 107class GetStatusTest(mox.MoxTestBase): 108 109 """Test case for _get_lab_status(). 110 111 We mock out dependencies on urllib2 and time.sleep(), and 112 confirm that the function returns the proper JSON representation 113 for a pre-defined response. 114 115 """ 116 117 def setUp(self): 118 super(GetStatusTest, self).setUp() 119 self.mox.StubOutWithMock(urllib2, 'urlopen') 120 self.mox.StubOutWithMock(time, 'sleep') 121 122 123 def test_success(self): 124 """Test that successful calls to urlopen() succeed.""" 125 json_string = _OPEN_STATUS_VALUES[0] 126 json_value = json.loads(json_string) 127 urllib2.urlopen(mox.IgnoreArg()).AndReturn( 128 _FakeURLResponse(200, json_string)) 129 self.mox.ReplayAll() 130 result = site_utils._get_lab_status(_FAKE_URL) 131 self.mox.VerifyAll() 132 self.assertEqual(json_value, result) 133 134 135 def test_retry_ioerror(self): 136 """Test that an IOError retries at least once.""" 137 json_string = _OPEN_STATUS_VALUES[0] 138 json_value = json.loads(json_string) 139 urllib2.urlopen(mox.IgnoreArg()).AndRaise( 140 IOError('Fake I/O error for a fake URL')) 141 time.sleep(mox.IgnoreArg()).AndReturn(None) 142 urllib2.urlopen(mox.IgnoreArg()).AndReturn( 143 _FakeURLResponse(200, json_string)) 144 self.mox.ReplayAll() 145 result = site_utils._get_lab_status(_FAKE_URL) 146 self.mox.VerifyAll() 147 self.assertEqual(json_value, result) 148 149 150 def test_retry_http_internal_error(self): 151 """Test that an HTTP error retries at least once.""" 152 json_string = _OPEN_STATUS_VALUES[0] 153 json_value = json.loads(json_string) 154 urllib2.urlopen(mox.IgnoreArg()).AndReturn( 155 _FakeURLResponse(500, '')) 156 time.sleep(mox.IgnoreArg()).AndReturn(None) 157 urllib2.urlopen(mox.IgnoreArg()).AndReturn( 158 _FakeURLResponse(200, json_string)) 159 self.mox.ReplayAll() 160 result = site_utils._get_lab_status(_FAKE_URL) 161 self.mox.VerifyAll() 162 self.assertEqual(json_value, result) 163 164 165 def test_failure_ioerror(self): 166 """Test that there's a failure if urlopen() never succeeds.""" 167 json_string = _OPEN_STATUS_VALUES[0] 168 json_value = json.loads(json_string) 169 for _ in range(site_utils._MAX_LAB_STATUS_ATTEMPTS): 170 urllib2.urlopen(mox.IgnoreArg()).AndRaise( 171 IOError('Fake I/O error for a fake URL')) 172 time.sleep(mox.IgnoreArg()).AndReturn(None) 173 self.mox.ReplayAll() 174 result = site_utils._get_lab_status(_FAKE_URL) 175 self.mox.VerifyAll() 176 self.assertEqual(None, result) 177 178 179 def test_failure_http_internal_error(self): 180 """Test that there's a failure for a permanent HTTP error.""" 181 json_string = _OPEN_STATUS_VALUES[0] 182 json_value = json.loads(json_string) 183 for _ in range(site_utils._MAX_LAB_STATUS_ATTEMPTS): 184 urllib2.urlopen(mox.IgnoreArg()).AndReturn( 185 _FakeURLResponse(404, 'Not here, never gonna be')) 186 time.sleep(mox.IgnoreArg()).InAnyOrder().AndReturn(None) 187 self.mox.ReplayAll() 188 result = site_utils._get_lab_status(_FAKE_URL) 189 self.mox.VerifyAll() 190 self.assertEqual(None, result) 191 192 193class DecodeStatusTest(unittest.TestCase): 194 195 """Test case for _decode_lab_status(). 196 197 Testing covers three distinct possible states: 198 1. Lab is up. All calls to _decode_lab_status() will 199 succeed without raising an exception. 200 2. Lab is down. All calls to _decode_lab_status() will 201 fail with TestLabException. 202 3. Build disabled. Calls to _decode_lab_status() will 203 succeed, except that board `_DEADBUILD` will raise 204 TestLabException. 205 206 """ 207 208 def _assert_lab_open(self, lab_status): 209 """Test that open status values are handled properly. 210 211 Test that _decode_lab_status() succeeds when the lab status 212 is up. 213 214 @param lab_status JSON value describing lab status. 215 216 """ 217 site_utils._decode_lab_status(lab_status, _LIVEBUILD) 218 site_utils._decode_lab_status(lab_status, _DEADBUILD) 219 220 221 def _assert_lab_closed(self, lab_status): 222 """Test that closed status values are handled properly. 223 224 Test that _decode_lab_status() raises TestLabException 225 when the lab status is down. 226 227 @param lab_status JSON value describing lab status. 228 229 """ 230 with self.assertRaises(site_utils.TestLabException): 231 site_utils._decode_lab_status(lab_status, _LIVEBUILD) 232 with self.assertRaises(site_utils.TestLabException): 233 site_utils._decode_lab_status(lab_status, _DEADBUILD) 234 235 236 def _assert_lab_deadbuild(self, lab_status): 237 """Test that disabled builds are handled properly. 238 239 Test that _decode_lab_status() raises TestLabException 240 for build `_DEADBUILD` and succeeds otherwise. 241 242 @param lab_status JSON value describing lab status. 243 244 """ 245 site_utils._decode_lab_status(lab_status, _LIVEBUILD) 246 with self.assertRaises(site_utils.TestLabException): 247 site_utils._decode_lab_status(lab_status, _DEADBUILD) 248 249 250 def _assert_lab_status(self, test_values, checker): 251 """General purpose test for _decode_lab_status(). 252 253 Decode each JSON string in `test_values`, and call the 254 `checker` function to test the corresponding status is 255 correctly handled. 256 257 @param test_values Array of JSON encoded strings representing 258 lab status. 259 @param checker Function to be called against each of the lab 260 status values in the `test_values` array. 261 262 """ 263 for s in test_values: 264 lab_status = json.loads(s) 265 checker(lab_status) 266 267 268 def test_open_lab(self): 269 """Test that open lab status values are handled correctly.""" 270 self._assert_lab_status(_OPEN_STATUS_VALUES, 271 self._assert_lab_open) 272 273 274 def test_closed_lab(self): 275 """Test that closed lab status values are handled correctly.""" 276 self._assert_lab_status(_CLOSED_STATUS_VALUES, 277 self._assert_lab_closed) 278 279 280 def test_dead_build(self): 281 """Test that disabled builds are handled correctly.""" 282 self._assert_lab_status(_DEADBUILD_STATUS_VALUES, 283 self._assert_lab_deadbuild) 284 285 286class CheckStatusTest(mox.MoxTestBase): 287 288 """Test case for `check_lab_status()`. 289 290 We mock out dependencies on `global_config.global_config()`, 291 `_get_lab_status()` and confirm that the function succeeds or 292 fails as expected. 293 294 N.B. We don't mock `_decode_lab_status()`; if DecodeStatusTest 295 is failing, this test may fail, too. 296 297 """ 298 299 def setUp(self): 300 super(CheckStatusTest, self).setUp() 301 self.mox.StubOutWithMock(global_config.global_config, 302 'get_config_value') 303 self.mox.StubOutWithMock(site_utils, '_get_lab_status') 304 305 306 def _setup_not_cautotest(self): 307 """Set up to mock the "we're not on cautotest" case.""" 308 global_config.global_config.get_config_value( 309 'SERVER', 'hostname').AndReturn('not-cautotest') 310 311 312 def _setup_no_status(self): 313 """Set up to mock lab status as unavailable.""" 314 global_config.global_config.get_config_value( 315 'SERVER', 'hostname').AndReturn('cautotest') 316 global_config.global_config.get_config_value( 317 'CROS', 'lab_status_url').AndReturn(_FAKE_URL) 318 site_utils._get_lab_status(_FAKE_URL).AndReturn(None) 319 320 321 def _setup_lab_status(self, json_string): 322 """Set up to mock a given lab status. 323 324 @param json_string JSON string for the JSON object to return 325 from `_get_lab_status()`. 326 327 """ 328 global_config.global_config.get_config_value( 329 'SERVER', 'hostname').AndReturn('cautotest') 330 global_config.global_config.get_config_value( 331 'CROS', 'lab_status_url').AndReturn(_FAKE_URL) 332 json_value = json.loads(json_string) 333 site_utils._get_lab_status(_FAKE_URL).AndReturn(json_value) 334 335 336 def _try_check_status(self, build): 337 """Test calling check_lab_status() with `build`.""" 338 try: 339 self.mox.ReplayAll() 340 site_utils.check_lab_status(build) 341 finally: 342 self.mox.VerifyAll() 343 344 345 def test_non_cautotest(self): 346 """Test a call with a build when the host isn't cautotest.""" 347 self._setup_not_cautotest() 348 self._try_check_status(_LIVEBUILD) 349 350 351 def test_no_lab_status(self): 352 """Test with a build when `_get_lab_status()` returns `None`.""" 353 self._setup_no_status() 354 self._try_check_status(_LIVEBUILD) 355 356 357 def test_lab_up_live_build(self): 358 """Test lab open with a build specified.""" 359 self._setup_lab_status(_OPEN_STATUS_VALUES[0]) 360 self._try_check_status(_LIVEBUILD) 361 362 363 def test_lab_down_live_build(self): 364 """Test lab closed with a build specified.""" 365 self._setup_lab_status(_CLOSED_STATUS_VALUES[0]) 366 with self.assertRaises(site_utils.TestLabException): 367 self._try_check_status(_LIVEBUILD) 368 369 370 def test_build_disabled_live_build(self): 371 """Test build disabled with a live build specified.""" 372 self._setup_lab_status(_DEADBUILD_STATUS_VALUES[0]) 373 self._try_check_status(_LIVEBUILD) 374 375 376 def test_build_disabled_dead_build(self): 377 """Test build disabled with the disabled build specified.""" 378 self._setup_lab_status(_DEADBUILD_STATUS_VALUES[0]) 379 with self.assertRaises(site_utils.TestLabException): 380 self._try_check_status(_DEADBUILD) 381 382 383if __name__ == '__main__': 384 unittest.main() 385