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