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