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