dev_server_unittest.py revision 580717f35931982b0a98fef941ecf445a8092348
1#!/usr/bin/python
2#
3# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Unit tests for client/common_lib/cros/dev_server.py."""
8
9import httplib
10import mox
11import StringIO
12import time
13import unittest
14import urllib2
15
16from autotest_lib.client.common_lib import error
17from autotest_lib.client.common_lib import global_config
18from autotest_lib.client.common_lib.cros import dev_server
19from autotest_lib.client.common_lib.cros import retry
20
21def retry_mock(ExceptionToCheck, timeout_min):
22  """A mock retry decorator to use in place of the actual one for testing.
23
24  @param ExceptionToCheck: the exception to check.
25  @param timeout_mins: Amount of time in mins to wait before timing out.
26
27  """
28  def inner_retry(func):
29    """The actual decorator.
30
31    @param func: Function to be called in decorator.
32
33    """
34    return func
35
36  return inner_retry
37
38
39class DevServerTest(mox.MoxTestBase):
40    """Unit tests for dev_server.DevServer.
41
42    @var _HOST: fake dev server host address.
43    """
44
45    _HOST = 'http://nothing'
46    _CRASH_HOST = 'http://nothing-crashed'
47    _CONFIG = global_config.global_config
48
49
50    def setUp(self):
51        super(DevServerTest, self).setUp()
52        self.crash_server = dev_server.CrashServer(DevServerTest._CRASH_HOST)
53        self.dev_server = dev_server.ImageServer(DevServerTest._HOST)
54        self.mox.StubOutWithMock(urllib2, 'urlopen')
55
56
57    def testSimpleResolve(self):
58        """One devserver, verify we resolve to it."""
59        self.mox.StubOutWithMock(dev_server, '_get_dev_server_list')
60        self.mox.StubOutWithMock(dev_server.DevServer, 'devserver_healthy')
61        dev_server._get_dev_server_list().AndReturn([DevServerTest._HOST])
62        dev_server.DevServer.devserver_healthy(DevServerTest._HOST).AndReturn(
63                                                                        True)
64        self.mox.ReplayAll()
65        devserver = dev_server.ImageServer.resolve('my_build')
66        self.assertEquals(devserver.url(), DevServerTest._HOST)
67
68
69    def testResolveWithFailure(self):
70        """Ensure we rehash on a failed ping on a bad_host."""
71        self.mox.StubOutWithMock(dev_server, '_get_dev_server_list')
72        bad_host, good_host = 'http://bad_host:99', 'http://good_host:8080'
73        dev_server._get_dev_server_list().AndReturn([bad_host, good_host])
74
75        # Mock out bad ping failure to bad_host by raising devserver exception.
76        urllib2.urlopen(mox.StrContains(bad_host), data=None).AndRaise(
77                dev_server.DevServerException())
78        # Good host is good.
79        to_return = StringIO.StringIO('{"free_disk": 1024}')
80        urllib2.urlopen(mox.StrContains(good_host), data=None).AndReturn(to_return)
81
82        self.mox.ReplayAll()
83        host = dev_server.ImageServer.resolve(0) # Using 0 as it'll hash to 0.
84        self.assertEquals(host.url(), good_host)
85        self.mox.VerifyAll()
86
87
88    def testResolveWithFailureURLError(self):
89        """Ensure we rehash on a failed ping on a bad_host after urlerror."""
90        # Retry mock just return the original method.
91        retry.retry = retry_mock
92        self.mox.StubOutWithMock(dev_server, '_get_dev_server_list')
93        bad_host, good_host = 'http://bad_host:99', 'http://good_host:8080'
94        dev_server._get_dev_server_list().AndReturn([bad_host, good_host])
95
96        # Mock out bad ping failure to bad_host by raising devserver exception.
97        urllib2.urlopen(mox.StrContains(bad_host),
98                data=None).MultipleTimes().AndRaise(
99                        urllib2.URLError('urlopen connection timeout'))
100
101        # Good host is good.
102        to_return = StringIO.StringIO('{"free_disk": 1024}')
103        urllib2.urlopen(mox.StrContains(good_host),
104                data=None).AndReturn(to_return)
105
106        self.mox.ReplayAll()
107        host = dev_server.ImageServer.resolve(0) # Using 0 as it'll hash to 0.
108        self.assertEquals(host.url(), good_host)
109        self.mox.VerifyAll()
110
111
112    def testResolveWithManyDevservers(self):
113        """Should be able to return different urls with multiple devservers."""
114        self.mox.StubOutWithMock(dev_server.ImageServer, 'servers')
115        self.mox.StubOutWithMock(dev_server.DevServer, 'devserver_healthy')
116
117        host0_expected = 'http://host0:8080'
118        host1_expected = 'http://host1:8082'
119
120        dev_server.ImageServer.servers().MultipleTimes().AndReturn(
121                [host0_expected, host1_expected])
122        dev_server.DevServer.devserver_healthy(host0_expected).AndReturn(True)
123        dev_server.DevServer.devserver_healthy(host1_expected).AndReturn(True)
124
125        self.mox.ReplayAll()
126        host0 = dev_server.ImageServer.resolve(0)
127        host1 = dev_server.ImageServer.resolve(1)
128        self.mox.VerifyAll()
129
130        self.assertEqual(host0.url(), host0_expected)
131        self.assertEqual(host1.url(), host1_expected)
132
133
134    def _returnHttpServerError(self):
135        e500 = urllib2.HTTPError(url='',
136                                 code=httplib.INTERNAL_SERVER_ERROR,
137                                 msg='',
138                                 hdrs=None,
139                                 fp=StringIO.StringIO('Expected.'))
140        urllib2.urlopen(mox.IgnoreArg()).AndRaise(e500)
141
142
143    def _returnHttpForbidden(self):
144        e403 = urllib2.HTTPError(url='',
145                                 code=httplib.FORBIDDEN,
146                                 msg='',
147                                 hdrs=None,
148                                 fp=StringIO.StringIO('Expected.'))
149        urllib2.urlopen(mox.IgnoreArg()).AndRaise(e403)
150
151
152    def testSuccessfulTriggerDownloadSync(self):
153        """Call the dev server's download method with synchronous=True."""
154        name = 'fake/image'
155        self.mox.StubOutWithMock(dev_server.ImageServer, 'finish_download')
156        to_return = StringIO.StringIO('Success')
157        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
158                                mox.StrContains(name),
159                                mox.StrContains('stage?'))).AndReturn(to_return)
160        to_return = StringIO.StringIO('True')
161        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
162                                mox.StrContains(name),
163                                mox.StrContains('is_staged'))).AndReturn(
164                                                                      to_return)
165        self.dev_server.finish_download(name)
166
167        # Synchronous case requires a call to finish download.
168        self.mox.ReplayAll()
169        self.dev_server.trigger_download(name, synchronous=True)
170        self.mox.VerifyAll()
171
172
173    def testSuccessfulTriggerDownloadASync(self):
174        """Call the dev server's download method with synchronous=False."""
175        name = 'fake/image'
176        to_return = StringIO.StringIO('Success')
177        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
178                                mox.StrContains(name),
179                                mox.StrContains('stage?'))).AndReturn(to_return)
180        to_return = StringIO.StringIO('True')
181        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
182                                mox.StrContains(name),
183                                mox.StrContains('is_staged'))).AndReturn(
184                                                                      to_return)
185
186        self.mox.ReplayAll()
187        self.dev_server.trigger_download(name, synchronous=False)
188        self.mox.VerifyAll()
189
190
191    def testURLErrorRetryTriggerDownload(self):
192        """Should retry on URLError, but pass through real exception."""
193        self.mox.StubOutWithMock(time, 'sleep')
194
195        refused = urllib2.URLError('[Errno 111] Connection refused')
196        urllib2.urlopen(mox.IgnoreArg()).AndRaise(refused)
197        time.sleep(mox.IgnoreArg())
198        self._returnHttpForbidden()
199        self.mox.ReplayAll()
200        self.assertRaises(dev_server.DevServerException,
201                          self.dev_server.trigger_download,
202                          '')
203
204
205    def testErrorTriggerDownload(self):
206        """Should call the dev server's download method, fail gracefully."""
207        self._returnHttpServerError()
208        self.mox.ReplayAll()
209        self.assertRaises(dev_server.DevServerException,
210                          self.dev_server.trigger_download,
211                          '')
212
213
214    def testForbiddenTriggerDownload(self):
215        """Should call the dev server's download method, get exception."""
216        self._returnHttpForbidden()
217        self.mox.ReplayAll()
218        self.assertRaises(dev_server.DevServerException,
219                          self.dev_server.trigger_download,
220                          '')
221
222
223    def testSuccessfulFinishDownload(self):
224        """Should successfully call the dev server's finish download method."""
225        name = 'fake/image'
226        to_return = StringIO.StringIO('Success')
227        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
228                                mox.StrContains(name),
229                                mox.StrContains('stage?'))).AndReturn(to_return)
230        to_return = StringIO.StringIO('True')
231        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
232                                mox.StrContains(name),
233                                mox.StrContains('is_staged'))).AndReturn(
234                                                                      to_return)
235
236        # Synchronous case requires a call to finish download.
237        self.mox.ReplayAll()
238        self.dev_server.finish_download(name)  # Raises on failure.
239        self.mox.VerifyAll()
240
241
242    def testErrorFinishDownload(self):
243        """Should call the dev server's finish download method, fail gracefully.
244        """
245        self._returnHttpServerError()
246        self.mox.ReplayAll()
247        self.assertRaises(dev_server.DevServerException,
248                          self.dev_server.finish_download,
249                          '')
250
251
252    def testListControlFiles(self):
253        """Should successfully list control files from the dev server."""
254        name = 'fake/build'
255        control_files = ['file/one', 'file/two']
256        to_return = StringIO.StringIO('\n'.join(control_files))
257        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
258                                mox.StrContains(name))).AndReturn(to_return)
259        self.mox.ReplayAll()
260        paths = self.dev_server.list_control_files(name)
261        self.assertEquals(len(paths), 2)
262        for f in control_files:
263            self.assertTrue(f in paths)
264
265
266    def testFailedListControlFiles(self):
267        """Should call the dev server's list-files method, get exception."""
268        self._returnHttpServerError()
269        self.mox.ReplayAll()
270        self.assertRaises(dev_server.DevServerException,
271                          self.dev_server.list_control_files,
272                          '')
273
274
275    def testExplodingListControlFiles(self):
276        """Should call the dev server's list-files method, get exception."""
277        self._returnHttpForbidden()
278        self.mox.ReplayAll()
279        self.assertRaises(dev_server.DevServerException,
280                          self.dev_server.list_control_files,
281                          '')
282
283
284    def testGetControlFile(self):
285        """Should successfully get a control file from the dev server."""
286        name = 'fake/build'
287        file = 'file/one'
288        contents = 'Multi-line\nControl File Contents\n'
289        to_return = StringIO.StringIO(contents)
290        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
291                                mox.StrContains(name),
292                                mox.StrContains(file))).AndReturn(to_return)
293        self.mox.ReplayAll()
294        self.assertEquals(self.dev_server.get_control_file(name, file),
295                          contents)
296
297
298    def testErrorGetControlFile(self):
299        """Should try to get the contents of a control file, get exception."""
300        self._returnHttpServerError()
301        self.mox.ReplayAll()
302        self.assertRaises(dev_server.DevServerException,
303                          self.dev_server.get_control_file,
304                          '', '')
305
306
307    def testForbiddenGetControlFile(self):
308        """Should try to get the contents of a control file, get exception."""
309        self._returnHttpForbidden()
310        self.mox.ReplayAll()
311        self.assertRaises(dev_server.DevServerException,
312                          self.dev_server.get_control_file,
313                          '', '')
314
315
316    def testGetLatestBuild(self):
317        """Should successfully return a build for a given target."""
318        self.mox.StubOutWithMock(dev_server.ImageServer, 'servers')
319        self.mox.StubOutWithMock(dev_server.DevServer, 'devserver_healthy')
320
321        dev_server.ImageServer.servers().AndReturn([self._HOST])
322        dev_server.DevServer.devserver_healthy(self._HOST).AndReturn(True)
323
324        target = 'x86-generic-release'
325        build_string = 'R18-1586.0.0-a1-b1514'
326        to_return = StringIO.StringIO(build_string)
327        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
328                                mox.StrContains(target))).AndReturn(to_return)
329        self.mox.ReplayAll()
330        build = dev_server.ImageServer.get_latest_build(target)
331        self.assertEquals(build_string, build)
332
333
334    def testGetLatestBuildWithManyDevservers(self):
335        """Should successfully return newest build with multiple devservers."""
336        self.mox.StubOutWithMock(dev_server.ImageServer, 'servers')
337        self.mox.StubOutWithMock(dev_server.DevServer, 'devserver_healthy')
338
339        host0_expected = 'http://host0:8080'
340        host1_expected = 'http://host1:8082'
341
342        dev_server.ImageServer.servers().MultipleTimes().AndReturn(
343                [host0_expected, host1_expected])
344
345        dev_server.DevServer.devserver_healthy(host0_expected).AndReturn(True)
346        dev_server.DevServer.devserver_healthy(host1_expected).AndReturn(True)
347
348        target = 'x86-generic-release'
349        build_string1 = 'R9-1586.0.0-a1-b1514'
350        build_string2 = 'R19-1586.0.0-a1-b3514'
351        to_return1 = StringIO.StringIO(build_string1)
352        to_return2 = StringIO.StringIO(build_string2)
353        urllib2.urlopen(mox.And(mox.StrContains(host0_expected),
354                                mox.StrContains(target))).AndReturn(to_return1)
355        urllib2.urlopen(mox.And(mox.StrContains(host1_expected),
356                                mox.StrContains(target))).AndReturn(to_return2)
357
358        self.mox.ReplayAll()
359        build = dev_server.ImageServer.get_latest_build(target)
360        self.assertEquals(build_string2, build)
361
362
363    def testCrashesAreSetToTheCrashServer(self):
364        """Should send symbolicate dump rpc calls to crash_server."""
365        hv = 'iliketacos'
366        self.mox.ReplayAll()
367        call = self.crash_server.build_call('symbolicate_dump')
368        self.assertTrue(call.startswith(self._CRASH_HOST))
369
370
371    def _stageTestHelper(self, artifacts=[], files=[], archive_url=None):
372        """Helper to test combos of files/artifacts/urls with stage call."""
373        expected_archive_url = archive_url
374        if not archive_url:
375            expected_archive_url = 'gs://my_default_url'
376            self.mox.StubOutWithMock(dev_server, '_get_image_storage_server')
377            dev_server._get_image_storage_server().AndReturn(
378                'gs://my_default_url')
379            name = 'fake/image'
380        else:
381            # This is embedded in the archive_url. Not needed.
382            name = ''
383
384        to_return = StringIO.StringIO('Success')
385        urllib2.urlopen(mox.And(mox.StrContains(expected_archive_url),
386                                mox.StrContains(name),
387                                mox.StrContains('artifacts=%s' %
388                                                ','.join(artifacts)),
389                                mox.StrContains('files=%s' % ','.join(files)),
390                                mox.StrContains('stage?'))).AndReturn(to_return)
391        to_return = StringIO.StringIO('True')
392        urllib2.urlopen(mox.And(mox.StrContains(expected_archive_url),
393                                mox.StrContains(name),
394                                mox.StrContains('artifacts=%s' %
395                                                ','.join(artifacts)),
396                                mox.StrContains('files=%s' % ','.join(files)),
397                                mox.StrContains('is_staged'))).AndReturn(
398                                        to_return)
399
400        self.mox.ReplayAll()
401        self.dev_server.stage_artifacts(name, artifacts, files, archive_url)
402        self.mox.VerifyAll()
403
404
405    def testStageArtifactsBasic(self):
406        """Basic functionality to stage artifacts (similar to trigger_download).
407        """
408        self._stageTestHelper(artifacts=['full_payload', 'stateful'])
409
410
411    def testStageArtifactsBasicWithFiles(self):
412        """Basic functionality to stage artifacts (similar to trigger_download).
413        """
414        self._stageTestHelper(artifacts=['full_payload', 'stateful'],
415                              files=['taco_bell.coupon'])
416
417
418    def testStageArtifactsOnlyFiles(self):
419        """Test staging of only file artifacts."""
420        self._stageTestHelper(files=['tasty_taco_bell.coupon'])
421
422
423    def testStageWithArchiveURL(self):
424        """Basic functionality to stage artifacts (similar to trigger_download).
425        """
426        self._stageTestHelper(files=['tasty_taco_bell.coupon'],
427                              archive_url='gs://tacos_galore/my/dir')
428
429
430    def testStagedFileUrl(self):
431        """Sanity tests that the staged file url looks right."""
432        devserver_label = 'x86-mario-release/R30-1234.0.0'
433        url = self.dev_server.get_staged_file_url('stateful.tgz',
434                                                  devserver_label)
435        expected_url = '/'.join([self._HOST, 'static', devserver_label,
436                                 'stateful.tgz'])
437        self.assertEquals(url, expected_url)
438
439        devserver_label = 'something_crazy/that/you_MIGHT/hate'
440        url = self.dev_server.get_staged_file_url('chromiumos_image.bin',
441                                                  devserver_label)
442        expected_url = '/'.join([self._HOST, 'static', devserver_label,
443                                 'chromiumos_image.bin'])
444        self.assertEquals(url, expected_url)
445
446
447    def _StageTimeoutHelper(self):
448        """Helper class for testing staging timeout."""
449        self.mox.StubOutWithMock(dev_server.ImageServer, 'call_and_wait')
450        dev_server.ImageServer.call_and_wait(
451                call_name='stage',
452                artifacts=mox.IgnoreArg(),
453                files=mox.IgnoreArg(),
454                archive_url=mox.IgnoreArg(),
455                error_message=mox.IgnoreArg()).AndRaise(error.TimeoutException)
456
457
458    def test_StageArtifactsTimeout(self):
459        """Test DevServerException is raised when stage_artifacts timed out."""
460        self._StageTimeoutHelper()
461        self.mox.ReplayAll()
462        self.assertRaises(dev_server.DevServerException,
463                          self.dev_server.stage_artifacts,
464                          image='fake/image', artifacts=['full_payload'])
465        self.mox.VerifyAll()
466
467
468    def test_TriggerDownloadTimeout(self):
469        """Test DevServerException is raised when trigger_download timed out."""
470        self._StageTimeoutHelper()
471        self.mox.ReplayAll()
472        self.assertRaises(dev_server.DevServerException,
473                          self.dev_server.trigger_download,
474                          image='fake/image')
475        self.mox.VerifyAll()
476
477
478    def test_FinishDownloadTimeout(self):
479        """Test DevServerException is raised when finish_download timed out."""
480        self._StageTimeoutHelper()
481        self.mox.ReplayAll()
482        self.assertRaises(dev_server.DevServerException,
483                          self.dev_server.finish_download,
484                          image='fake/image')
485        self.mox.VerifyAll()
486
487
488    def test_compare_load(self):
489        """Test load comparison logic.
490        """
491        load_high_cpu = {'devserver': 'http://devserver_1:8082',
492                         dev_server.DevServer.CPU_LOAD: 100.0,
493                         dev_server.DevServer.NETWORK_IO: 1024*1024*1.0,
494                         dev_server.DevServer.DISK_IO: 1024*1024.0}
495        load_high_network = {'devserver': 'http://devserver_1:8082',
496                             dev_server.DevServer.CPU_LOAD: 1.0,
497                             dev_server.DevServer.NETWORK_IO: 1024*1024*100.0,
498                             dev_server.DevServer.DISK_IO: 1024*1024*1.0}
499        load_1 = {'devserver': 'http://devserver_1:8082',
500                  dev_server.DevServer.CPU_LOAD: 1.0,
501                  dev_server.DevServer.NETWORK_IO: 1024*1024*1.0,
502                  dev_server.DevServer.DISK_IO: 1024*1024*2.0}
503        load_2 = {'devserver': 'http://devserver_1:8082',
504                  dev_server.DevServer.CPU_LOAD: 1.0,
505                  dev_server.DevServer.NETWORK_IO: 1024*1024*1.0,
506                  dev_server.DevServer.DISK_IO: 1024*1024*1.0}
507        self.assertFalse(dev_server._is_load_healthy(load_high_cpu))
508        self.assertFalse(dev_server._is_load_healthy(load_high_network))
509        self.assertTrue(dev_server._compare_load(load_1, load_2) > 0)
510
511
512if __name__ == "__main__":
513    unittest.main()
514