blob: f241dad6c688fd1a6c2169f3c21d5b36d95abb73 [file] [log] [blame]
Chris Masone6fed6462011-10-20 16:36:43 -07001#!/usr/bin/python
2#
Chris Masoneab3e7332012-02-29 18:54:58 -08003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Chris Masone6fed6462011-10-20 16:36:43 -07004# 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 server/cros/dynamic_suite.py."""
8
9import logging
10import mox
Chris Masoned368cc42012-03-07 15:16:59 -080011import random
Chris Masone6fed6462011-10-20 16:36:43 -070012import shutil
13import tempfile
14import time
15import unittest
16
Chris Masonef8b53062012-05-08 22:14:18 -070017from autotest_lib.client.common_lib import base_job, control_data, error
18from autotest_lib.client.common_lib import global_config
Chris Masone47c9e642012-04-25 14:22:18 -070019from autotest_lib.frontend.afe.json_rpc import proxy
Chris Masone6fed6462011-10-20 16:36:43 -070020from autotest_lib.server.cros import control_file_getter, dynamic_suite
21from autotest_lib.server import frontend
22
23class FakeJob(object):
24 """Faked out RPC-client-side Job object."""
25 def __init__(self, id=0, statuses=[]):
26 self.id = id
Chris Masoned368cc42012-03-07 15:16:59 -080027 self.hostname = 'host%d' % id
Scott Zawalskie5bb1c52012-02-29 13:15:50 -050028 self.owner = 'tester'
Chris Masone6fed6462011-10-20 16:36:43 -070029 self.name = 'Fake Job %d' % self.id
30 self.statuses = statuses
31
32
Chris Masone5374c672012-03-05 15:11:39 -080033class FakeHost(object):
34 """Faked out RPC-client-side Host object."""
35 def __init__(self, status='Ready'):
36 self.status = status
37
Chris Masoned368cc42012-03-07 15:16:59 -080038class FakeLabel(object):
39 """Faked out RPC-client-side Label object."""
40 def __init__(self, id=0):
41 self.id = id
42
Chris Masone5374c672012-03-05 15:11:39 -080043
Chris Masoneab3e7332012-02-29 18:54:58 -080044class DynamicSuiteTest(mox.MoxTestBase):
45 """Unit tests for dynamic_suite module methods.
46
47 @var _DARGS: default args to vet.
48 """
49
50
51 def setUp(self):
52 super(DynamicSuiteTest, self).setUp()
53 self._DARGS = {'name': 'name',
54 'build': 'build',
55 'board': 'board',
56 'job': self.mox.CreateMock(base_job.base_job),
57 'num': 1,
58 'pool': 'pool',
59 'skip_reimage': True,
Chris Masone62579122012-03-08 15:18:43 -080060 'check_hosts': False,
Chris Masoneab3e7332012-02-29 18:54:58 -080061 'add_experimental': False}
62
63
64 def testVetRequiredReimageAndRunArgs(self):
65 """Should verify only that required args are present and correct."""
Chris Masone62579122012-03-08 15:18:43 -080066 build, board, name, job, _, _, _, _,_ = \
Chris Masoneab3e7332012-02-29 18:54:58 -080067 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
68 self.assertEquals(build, self._DARGS['build'])
69 self.assertEquals(board, self._DARGS['board'])
70 self.assertEquals(name, self._DARGS['name'])
71 self.assertEquals(job, self._DARGS['job'])
72
73
74 def testVetReimageAndRunBuildArgFail(self):
75 """Should fail verification because |build| arg is bad."""
76 self._DARGS['build'] = None
Chris Masonef8b53062012-05-08 22:14:18 -070077 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -080078 dynamic_suite._vet_reimage_and_run_args,
79 **self._DARGS)
80
81
82 def testVetReimageAndRunBoardArgFail(self):
83 """Should fail verification because |board| arg is bad."""
84 self._DARGS['board'] = None
Chris Masonef8b53062012-05-08 22:14:18 -070085 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -080086 dynamic_suite._vet_reimage_and_run_args,
87 **self._DARGS)
88
89
90 def testVetReimageAndRunNameArgFail(self):
91 """Should fail verification because |name| arg is bad."""
92 self._DARGS['name'] = None
Chris Masonef8b53062012-05-08 22:14:18 -070093 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -080094 dynamic_suite._vet_reimage_and_run_args,
95 **self._DARGS)
96
97
98 def testVetReimageAndRunJobArgFail(self):
99 """Should fail verification because |job| arg is bad."""
100 self._DARGS['job'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700101 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -0800102 dynamic_suite._vet_reimage_and_run_args,
103 **self._DARGS)
104
105
106 def testOverrideOptionalReimageAndRunArgs(self):
107 """Should verify that optional args can be overridden."""
Chris Masone62579122012-03-08 15:18:43 -0800108 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800109 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
110 self.assertEquals(pool, self._DARGS['pool'])
111 self.assertEquals(num, self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800112 self.assertEquals(check, self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800113 self.assertEquals(skip, self._DARGS['skip_reimage'])
114 self.assertEquals(expr, self._DARGS['add_experimental'])
115
116
117 def testDefaultOptionalReimageAndRunArgs(self):
118 """Should verify that optional args get defaults."""
119 del(self._DARGS['pool'])
120 del(self._DARGS['skip_reimage'])
Chris Masone62579122012-03-08 15:18:43 -0800121 del(self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800122 del(self._DARGS['add_experimental'])
123 del(self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800124 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800125 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
126 self.assertEquals(pool, None)
127 self.assertEquals(num, None)
Chris Masone62579122012-03-08 15:18:43 -0800128 self.assertEquals(check, True)
Chris Masoneab3e7332012-02-29 18:54:58 -0800129 self.assertEquals(skip, False)
130 self.assertEquals(expr, True)
131
132
Chris Masone6fed6462011-10-20 16:36:43 -0700133class ReimagerTest(mox.MoxTestBase):
134 """Unit tests for dynamic_suite.Reimager.
135
136 @var _URL: fake image url
Chris Masone8b7cd422012-02-22 13:16:11 -0800137 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700138 @var _NUM: fake number of machines to run on
139 @var _BOARD: fake board to reimage
140 """
141
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800142 _URL = 'http://nothing/%s'
Chris Masone8b7cd422012-02-22 13:16:11 -0800143 _BUILD = 'build'
Chris Masone6fed6462011-10-20 16:36:43 -0700144 _NUM = 4
145 _BOARD = 'board'
Chris Masone5552dd72012-02-15 15:01:04 -0800146 _CONFIG = global_config.global_config
Chris Masone6fed6462011-10-20 16:36:43 -0700147
148
149 def setUp(self):
150 super(ReimagerTest, self).setUp()
151 self.afe = self.mox.CreateMock(frontend.AFE)
152 self.tko = self.mox.CreateMock(frontend.TKO)
153 self.reimager = dynamic_suite.Reimager('', afe=self.afe, tko=self.tko)
Chris Masone5552dd72012-02-15 15:01:04 -0800154 self._CONFIG.override_config_value('CROS',
155 'sharding_factor',
156 "%d" % self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700157
158
159 def testEnsureVersionLabelAlreadyExists(self):
Chris Masone47c9e642012-04-25 14:22:18 -0700160 """Should tolerate a label that already exists."""
Chris Masone6fed6462011-10-20 16:36:43 -0700161 name = 'label'
Chris Masone47c9e642012-04-25 14:22:18 -0700162 error = proxy.ValidationError(
163 {'name': 'ValidationError',
164 'message': '{"name": "This value must be unique"}',
165 'traceback': ''},
166 'BAD')
167 self.afe.create_label(name=name).AndRaise(error)
Chris Masone6fed6462011-10-20 16:36:43 -0700168 self.mox.ReplayAll()
169 self.reimager._ensure_version_label(name)
170
171
172 def testEnsureVersionLabel(self):
173 """Should create a label if it doesn't already exist."""
174 name = 'label'
Chris Masone6fed6462011-10-20 16:36:43 -0700175 self.afe.create_label(name=name)
176 self.mox.ReplayAll()
177 self.reimager._ensure_version_label(name)
178
179
Chris Masone5374c672012-03-05 15:11:39 -0800180 def testCountHostsByBoardAndPool(self):
181 """Should count available hosts by board and pool."""
182 spec = [self._BOARD, 'pool:bvt']
183 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()])
184 self.mox.ReplayAll()
185 self.assertEquals(self.reimager._count_usable_hosts(spec), 1)
186
187
188 def testCountHostsByBoard(self):
189 """Should count available hosts by board."""
190 spec = [self._BOARD]
191 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()] * 2)
192 self.mox.ReplayAll()
193 self.assertEquals(self.reimager._count_usable_hosts(spec), 2)
194
195
196 def testCountZeroHostsByBoard(self):
197 """Should count the available hosts, by board, getting zero."""
198 spec = [self._BOARD]
199 self.afe.get_hosts(multiple_labels=spec).AndReturn([])
200 self.mox.ReplayAll()
201 self.assertEquals(self.reimager._count_usable_hosts(spec), 0)
202
203
Chris Masone6fed6462011-10-20 16:36:43 -0700204 def testInjectVars(self):
205 """Should inject dict of varibles into provided strings."""
206 def find_all_in(d, s):
207 """Returns true if all key-value pairs in |d| are printed in |s|."""
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800208 for k,v in d.iteritems():
209 if isinstance(v, str):
210 if "%s='%s'\n" % (k,v) not in s:
211 return False
212 else:
213 if "%s=%r\n" % (k,v) not in s:
214 return False
215 return True
Chris Masone6fed6462011-10-20 16:36:43 -0700216
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800217 v = {'v1': 'one', 'v2': 'two', 'v3': None, 'v4': False, 'v5': 5}
Chris Masone8b764252012-01-17 11:12:51 -0800218 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, '')))
219 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, 'ctrl')))
Chris Masone6fed6462011-10-20 16:36:43 -0700220
221
222 def testReportResultsGood(self):
223 """Should report results in the case where all jobs passed."""
224 job = self.mox.CreateMock(frontend.Job)
225 job.name = 'RPC Client job'
226 job.result = True
227 recorder = self.mox.CreateMock(base_job.base_job)
228 recorder.record('GOOD', mox.IgnoreArg(), job.name)
229 self.mox.ReplayAll()
230 self.reimager._report_results(job, recorder.record)
231
232
233 def testReportResultsBad(self):
234 """Should report results in various job failure cases.
235
236 In this test scenario, there are five hosts, all the 'netbook' platform.
237
238 h1: Did not run
239 h2: Two failed tests
240 h3: Two aborted tests
241 h4: completed, GOOD
242 h5: completed, GOOD
243 """
244 H1 = 'host1'
245 H2 = 'host2'
246 H3 = 'host3'
247 H4 = 'host4'
248 H5 = 'host5'
249
250 class FakeResult(object):
251 def __init__(self, reason):
252 self.reason = reason
253
254
255 # The RPC-client-side Job object that is annotated with results.
256 job = FakeJob()
257 job.result = None # job failed, there are results to report.
258
259 # The semantics of |results_platform_map| and |test_results| are
260 # drawn from frontend.AFE.poll_all_jobs()
261 job.results_platform_map = {'netbook': {'Aborted' : [H3],
Chris Masone8b7cd422012-02-22 13:16:11 -0800262 'Completed' : [H1, H4, H5],
263 'Failed': [H2]
Chris Masone6fed6462011-10-20 16:36:43 -0700264 }
265 }
266 # Gin up fake results for H2 and H3 failure cases.
267 h2 = frontend.TestResults()
268 h2.fail = [FakeResult('a'), FakeResult('b')]
269 h3 = frontend.TestResults()
270 h3.fail = [FakeResult('a'), FakeResult('b')]
271 # Skipping H1 in |test_status| dict means that it did not get run.
272 job.test_status = {H2: h2, H3: h3, H4: {}, H5: {}}
273
274 # Set up recording expectations.
275 rjob = self.mox.CreateMock(base_job.base_job)
276 for res in h2.fail:
277 rjob.record('FAIL', mox.IgnoreArg(), H2, res.reason).InAnyOrder()
278 for res in h3.fail:
279 rjob.record('ABORT', mox.IgnoreArg(), H3, res.reason).InAnyOrder()
280 rjob.record('GOOD', mox.IgnoreArg(), H4).InAnyOrder()
281 rjob.record('GOOD', mox.IgnoreArg(), H5).InAnyOrder()
282 rjob.record(
283 'ERROR', mox.IgnoreArg(), H1, mox.IgnoreArg()).InAnyOrder()
284
285 self.mox.ReplayAll()
286 self.reimager._report_results(job, rjob.record)
287
288
289 def testScheduleJob(self):
290 """Should be able to create a job with the AFE."""
291 # Fake out getting the autoupdate control file contents.
292 cf_getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
293 cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
294 self.reimager._cf_getter = cf_getter
295
Chris Masone6dca10a2012-02-15 15:41:42 -0800296 self._CONFIG.override_config_value('CROS',
297 'image_url_pattern',
298 self._URL)
Chris Masone6fed6462011-10-20 16:36:43 -0700299 self.afe.create_job(
Chris Masone8b7cd422012-02-22 13:16:11 -0800300 control_file=mox.And(mox.StrContains(self._BUILD),
301 mox.StrContains(self._URL % self._BUILD)),
302 name=mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700303 control_type='Server',
Chris Masone5374c672012-03-05 15:11:39 -0800304 meta_hosts=[self._BOARD] * self._NUM,
Chris Masone99378582012-04-30 13:10:58 -0700305 dependencies=[],
306 priority='Low')
Chris Masone6fed6462011-10-20 16:36:43 -0700307 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700308 self.reimager._schedule_reimage_job(self._BUILD, self._BOARD, None,
309 self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700310
311
Chris Masone62579122012-03-08 15:18:43 -0800312 def expect_attempt(self, success, ex=None, check_hosts=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700313 """Sets up |self.reimager| to expect an attempt() that returns |success|
314
Chris Masoned368cc42012-03-07 15:16:59 -0800315 Also stubs out Reimger._clear_build_state(), should the caller wish
316 to set an expectation there as well.
317
Chris Masone796fcf12012-02-22 16:53:31 -0800318 @param success: the value returned by poll_job_results()
319 @param ex: if not None, |ex| is raised by get_jobs()
Chris Masone6fed6462011-10-20 16:36:43 -0700320 @return a FakeJob configured with appropriate expectations
321 """
322 canary = FakeJob()
323 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
Chris Masone8b7cd422012-02-22 13:16:11 -0800324 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone6fed6462011-10-20 16:36:43 -0700325
326 self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
Chris Masone8b7cd422012-02-22 13:16:11 -0800327 self.reimager._schedule_reimage_job(self._BUILD,
Chris Masonec43448f2012-05-31 12:55:59 -0700328 self._BOARD,
329 None,
330 self._NUM).AndReturn(canary)
Chris Masone62579122012-03-08 15:18:43 -0800331 if check_hosts:
332 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
333 self.reimager._count_usable_hosts(
334 mox.IgnoreArg()).AndReturn(self._NUM)
Chris Masone5374c672012-03-05 15:11:39 -0800335
Chris Masone6fed6462011-10-20 16:36:43 -0700336 if success is not None:
337 self.mox.StubOutWithMock(self.reimager, '_report_results')
338 self.reimager._report_results(canary, mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800339 canary.results_platform_map = {None: {'Total': [canary.hostname]}}
340
Chris Masone6fed6462011-10-20 16:36:43 -0700341
342 self.afe.get_jobs(id=canary.id, not_yet_run=True).AndReturn([])
Chris Masone796fcf12012-02-22 16:53:31 -0800343 if ex is not None:
344 self.afe.get_jobs(id=canary.id, finished=True).AndRaise(ex)
345 else:
346 self.afe.get_jobs(id=canary.id, finished=True).AndReturn([canary])
347 self.afe.poll_job_results(mox.IgnoreArg(),
348 canary, 0).AndReturn(success)
Chris Masone6fed6462011-10-20 16:36:43 -0700349
Chris Masoned368cc42012-03-07 15:16:59 -0800350 self.mox.StubOutWithMock(self.reimager, '_clear_build_state')
351
Chris Masone6fed6462011-10-20 16:36:43 -0700352 return canary
353
354
355 def testSuccessfulReimage(self):
356 """Should attempt a reimage and record success."""
Chris Masone62579122012-03-08 15:18:43 -0800357 canary = self.expect_attempt(success=True)
Chris Masone6fed6462011-10-20 16:36:43 -0700358
359 rjob = self.mox.CreateMock(base_job.base_job)
360 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
361 rjob.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800362 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
Chris Masone6fed6462011-10-20 16:36:43 -0700363 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700364 self.reimager.attempt(self._BUILD, self._BOARD, None, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800365 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700366
367
368 def testFailedReimage(self):
369 """Should attempt a reimage and record failure."""
Chris Masone62579122012-03-08 15:18:43 -0800370 canary = self.expect_attempt(success=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700371
372 rjob = self.mox.CreateMock(base_job.base_job)
373 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
374 rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800375 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
Chris Masone6fed6462011-10-20 16:36:43 -0700376 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700377 self.reimager.attempt(self._BUILD, self._BOARD, None, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800378 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700379
380
381 def testReimageThatNeverHappened(self):
382 """Should attempt a reimage and record that it didn't run."""
Chris Masone62579122012-03-08 15:18:43 -0800383 canary = self.expect_attempt(success=None)
Chris Masone6fed6462011-10-20 16:36:43 -0700384
385 rjob = self.mox.CreateMock(base_job.base_job)
386 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
387 rjob.record('FAIL', mox.IgnoreArg(), canary.name, mox.IgnoreArg())
388 rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
389 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700390 self.reimager.attempt(self._BUILD, self._BOARD, None, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800391 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700392
393
Chris Masone796fcf12012-02-22 16:53:31 -0800394 def testReimageThatRaised(self):
395 """Should attempt a reimage that raises an exception and record that."""
396 ex_message = 'Oh no!'
Chris Masone62579122012-03-08 15:18:43 -0800397 canary = self.expect_attempt(success=None, ex=Exception(ex_message))
Chris Masone796fcf12012-02-22 16:53:31 -0800398
399 rjob = self.mox.CreateMock(base_job.base_job)
400 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
401 rjob.record('END ERROR', mox.IgnoreArg(), mox.IgnoreArg(), ex_message)
402 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700403 self.reimager.attempt(self._BUILD, self._BOARD, None, rjob.record, True)
Chris Masone62579122012-03-08 15:18:43 -0800404 self.reimager.clear_reimaged_host_state(self._BUILD)
405
406
407 def testSuccessfulReimageThatCouldNotScheduleRightAway(self):
Chris Masone99378582012-04-30 13:10:58 -0700408 """Should attempt reimage, ignoring host availability; record success.
Chris Masone62579122012-03-08 15:18:43 -0800409 """
410 canary = self.expect_attempt(success=True, check_hosts=False)
411
412 rjob = self.mox.CreateMock(base_job.base_job)
413 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
414 rjob.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
415 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
416 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700417 self.reimager.attempt(self._BUILD, self._BOARD, None, rjob.record,
418 False)
Chris Masoned368cc42012-03-07 15:16:59 -0800419 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone796fcf12012-02-22 16:53:31 -0800420
421
Chris Masone5374c672012-03-05 15:11:39 -0800422 def testReimageThatCouldNotSchedule(self):
423 """Should attempt a reimage that can't be scheduled."""
Chris Masone502b71e2012-04-10 10:41:35 -0700424 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
425 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
426
427 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
428 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(1)
429
430 rjob = self.mox.CreateMock(base_job.base_job)
431 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
432 rjob.record('END WARN', mox.IgnoreArg(), mox.IgnoreArg(),
433 mox.StrContains('Too few hosts'))
Chris Masone502b71e2012-04-10 10:41:35 -0700434 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700435 self.reimager.attempt(self._BUILD, self._BOARD, None, rjob.record, True)
Chris Masone502b71e2012-04-10 10:41:35 -0700436 self.reimager.clear_reimaged_host_state(self._BUILD)
437
438
439 def testReimageWithNoAvailableHosts(self):
440 """Should attempt a reimage while all hosts are dead."""
Chris Masone62579122012-03-08 15:18:43 -0800441 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
442 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone5374c672012-03-05 15:11:39 -0800443
444 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
445 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(0)
446
447 rjob = self.mox.CreateMock(base_job.base_job)
448 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
Chris Masone502b71e2012-04-10 10:41:35 -0700449 rjob.record('END ERROR', mox.IgnoreArg(), mox.IgnoreArg(),
450 mox.StrContains('All hosts'))
Chris Masone5374c672012-03-05 15:11:39 -0800451 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700452 self.reimager.attempt(self._BUILD, self._BOARD, None, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800453 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone5374c672012-03-05 15:11:39 -0800454
455
Chris Masone99378582012-04-30 13:10:58 -0700456class StatusContains(mox.Comparator):
457 @staticmethod
458 def CreateFromStrings(status=None, test_name=None, reason=None):
459 status_comp = mox.StrContains(status) if status else mox.IgnoreArg()
460 name_comp = mox.StrContains(test_name) if test_name else mox.IgnoreArg()
461 reason_comp = mox.StrContains(reason) if reason else mox.IgnoreArg()
462 return StatusContains(status_comp, name_comp, reason_comp)
463
464
465 def __init__(self, status=mox.IgnoreArg(), test_name=mox.IgnoreArg(),
466 reason=mox.IgnoreArg()):
467 """Initialize.
468
469 Takes mox.Comparator objects to apply to dynamic_suite.Status
470 member variables.
471
472 @param status: status code, e.g. 'INFO', 'START', etc.
473 @param test_name: expected test name.
474 @param reason: expected reason
475 """
476 self._status = status
477 self._test_name = test_name
478 self._reason = reason
479
480
481 def equals(self, rhs):
Chris Masoneb0a95612012-05-31 14:25:03 -0700482 """Check to see if fields match base_job.status_log_entry obj in rhs.
Chris Masone99378582012-04-30 13:10:58 -0700483
Chris Masoneb0a95612012-05-31 14:25:03 -0700484 @param rhs: base_job.status_log_entry object to match.
Chris Masone99378582012-04-30 13:10:58 -0700485 @return boolean
486 """
487 return (self._status.equals(rhs.status_code) and
488 self._test_name.equals(rhs.operation) and
489 self._reason.equals(rhs.message))
490
491
492 def __repr__(self):
493 return '<Status containing \'%s\t%s\t%s\'>' % (self._status,
494 self._test_name,
495 self._reason)
496
497
Chris Masone6fed6462011-10-20 16:36:43 -0700498class SuiteTest(mox.MoxTestBase):
499 """Unit tests for dynamic_suite.Suite.
500
Chris Masone8b7cd422012-02-22 13:16:11 -0800501 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700502 @var _TAG: fake suite tag
503 """
504
Chris Masone8b7cd422012-02-22 13:16:11 -0800505 _BUILD = 'build'
506 _TAG = 'suite_tag'
Chris Masone6fed6462011-10-20 16:36:43 -0700507
508
Chris Masoneed356392012-05-08 14:07:13 -0700509 class FakeControlData(object):
510 """A fake parsed control file data structure."""
511 def __init__(self, data, expr=False):
512 self.string = 'text-' + data
513 self.name = 'name-' + data
514 self.data = data
515 self.suite = SuiteTest._TAG
516 self.test_type = 'Client'
517 self.experimental = expr
518
519
Chris Masone6fed6462011-10-20 16:36:43 -0700520 def setUp(self):
Chris Masone6fed6462011-10-20 16:36:43 -0700521 super(SuiteTest, self).setUp()
522 self.afe = self.mox.CreateMock(frontend.AFE)
523 self.tko = self.mox.CreateMock(frontend.TKO)
524
525 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__)
526
527 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
528
Chris Masoneed356392012-05-08 14:07:13 -0700529 self.files = {'one': SuiteTest.FakeControlData('data_one', expr=True),
530 'two': SuiteTest.FakeControlData('data_two'),
531 'three': SuiteTest.FakeControlData('data_three')}
Chris Masone6fed6462011-10-20 16:36:43 -0700532
Chris Masone75a20612012-05-08 12:37:31 -0700533 self.files_to_filter = {
Chris Masoneed356392012-05-08 14:07:13 -0700534 'with/deps/...': SuiteTest.FakeControlData('...gets filtered'),
535 'with/profilers/...': SuiteTest.FakeControlData('...gets filtered')}
Chris Masone75a20612012-05-08 12:37:31 -0700536
Chris Masone6fed6462011-10-20 16:36:43 -0700537
538 def tearDown(self):
539 super(SuiteTest, self).tearDown()
540 shutil.rmtree(self.tmpdir, ignore_errors=True)
541
542
543 def expect_control_file_parsing(self):
544 """Expect an attempt to parse the 'control files' in |self.files|."""
Chris Masone75a20612012-05-08 12:37:31 -0700545 all_files = self.files.keys() + self.files_to_filter.keys()
Chris Masoneed356392012-05-08 14:07:13 -0700546 self._set_control_file_parsing_expectations(False, all_files,
547 self.files.iteritems())
548
549
550 def expect_control_file_reparsing(self):
551 """Expect re-parsing the 'control files' in |self.files|."""
552 all_files = self.files.keys() + self.files_to_filter.keys()
553 self._set_control_file_parsing_expectations(True, all_files,
554 self.files.iteritems())
555
556
557 def expect_racy_control_file_reparsing(self, new_files):
558 """Expect re-fetching and parsing of control files to return extra.
559
560 @param new_files: extra control files that showed up during scheduling.
561 """
562 all_files = (self.files.keys() + self.files_to_filter.keys() +
563 new_files.keys())
564 new_files.update(self.files)
565 self._set_control_file_parsing_expectations(True, all_files,
566 new_files.iteritems())
567
568
569 def _set_control_file_parsing_expectations(self, already_stubbed,
570 file_list, files_to_parse):
571 """Expect an attempt to parse the 'control files' in |files|.
572
573 @param already_stubbed: parse_control_string already stubbed out.
574 @param file_list: the files the dev server returns
575 @param files_to_parse: the {'name': FakeControlData} dict of files we
576 expect to get parsed.
577 """
578 if not already_stubbed:
579 self.mox.StubOutWithMock(control_data, 'parse_control_string')
580
581 self.getter.get_control_file_list().AndReturn(file_list)
582 for file, data in files_to_parse:
583 self.getter.get_control_file_contents(
584 file).InAnyOrder().AndReturn(data.string)
Chris Masone6fed6462011-10-20 16:36:43 -0700585 control_data.parse_control_string(
Chris Masoneed356392012-05-08 14:07:13 -0700586 data.string, raise_warnings=True).InAnyOrder().AndReturn(data)
Chris Masone6fed6462011-10-20 16:36:43 -0700587
588
589 def testFindAndParseStableTests(self):
590 """Should find only non-experimental tests that match a predicate."""
591 self.expect_control_file_parsing()
592 self.mox.ReplayAll()
593
594 predicate = lambda d: d.text == self.files['two'].string
595 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter, predicate)
596 self.assertEquals(len(tests), 1)
597 self.assertEquals(tests[0], self.files['two'])
598
599
600 def testFindAndParseTests(self):
601 """Should find all tests that match a predicate."""
602 self.expect_control_file_parsing()
603 self.mox.ReplayAll()
604
605 predicate = lambda d: d.text != self.files['two'].string
606 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter,
607 predicate,
608 add_experimental=True)
609 self.assertEquals(len(tests), 2)
610 self.assertTrue(self.files['one'] in tests)
611 self.assertTrue(self.files['three'] in tests)
612
613
614 def mock_control_file_parsing(self):
615 """Fake out find_and_parse_tests(), returning content from |self.files|.
616 """
617 for test in self.files.values():
618 test.text = test.string # mimic parsing.
619 self.mox.StubOutWithMock(dynamic_suite.Suite, 'find_and_parse_tests')
620 dynamic_suite.Suite.find_and_parse_tests(
621 mox.IgnoreArg(),
622 mox.IgnoreArg(),
623 add_experimental=True).AndReturn(self.files.values())
624
625
626 def testStableUnstableFilter(self):
627 """Should distinguish between experimental and stable tests."""
628 self.mock_control_file_parsing()
629 self.mox.ReplayAll()
630 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
Chris Masoned6f38c82012-02-22 14:53:42 -0800631 afe=self.afe, tko=self.tko)
Chris Masone6fed6462011-10-20 16:36:43 -0700632
633 self.assertTrue(self.files['one'] in suite.tests)
634 self.assertTrue(self.files['two'] in suite.tests)
635 self.assertTrue(self.files['one'] in suite.unstable_tests())
636 self.assertTrue(self.files['two'] in suite.stable_tests())
637 self.assertFalse(self.files['one'] in suite.stable_tests())
638 self.assertFalse(self.files['two'] in suite.unstable_tests())
639
640
641 def expect_job_scheduling(self, add_experimental):
642 """Expect jobs to be scheduled for 'tests' in |self.files|.
643
644 @param add_experimental: expect jobs for experimental tests as well.
645 """
646 for test in self.files.values():
647 if not add_experimental and test.experimental:
648 continue
649 self.afe.create_job(
650 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800651 name=mox.And(mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700652 mox.StrContains(test.name)),
653 control_type=mox.IgnoreArg(),
Chris Masone8b7cd422012-02-22 13:16:11 -0800654 meta_hosts=[dynamic_suite.VERSION_PREFIX + self._BUILD],
Chris Masonebafbbb02012-05-16 13:41:36 -0700655 dependencies=[],
656 keyvals={'build': self._BUILD, 'suite': self._TAG}
657 ).AndReturn(FakeJob())
Chris Masone6fed6462011-10-20 16:36:43 -0700658
659
660 def testScheduleTests(self):
661 """Should schedule stable and experimental tests with the AFE."""
662 self.mock_control_file_parsing()
663 self.expect_job_scheduling(add_experimental=True)
664
665 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800666 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800667 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800668 suite.schedule()
Chris Masone6fed6462011-10-20 16:36:43 -0700669
670
Scott Zawalski9ece6532012-02-28 14:10:47 -0500671 def testScheduleTestsAndRecord(self):
672 """Should schedule stable and experimental tests with the AFE."""
673 self.mock_control_file_parsing()
674 self.mox.ReplayAll()
675 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
676 afe=self.afe, tko=self.tko,
677 results_dir=self.tmpdir)
678 self.mox.ResetAll()
679 self.expect_job_scheduling(add_experimental=True)
680 self.mox.StubOutWithMock(suite, '_record_scheduled_jobs')
681 suite._record_scheduled_jobs()
682 self.mox.ReplayAll()
683 suite.schedule()
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500684 for job in suite._jobs:
685 self.assertTrue(hasattr(job, 'test_name'))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500686
687
Chris Masone6fed6462011-10-20 16:36:43 -0700688 def testScheduleStableTests(self):
689 """Should schedule only stable tests with the AFE."""
690 self.mock_control_file_parsing()
691 self.expect_job_scheduling(add_experimental=False)
692
693 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800694 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800695 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800696 suite.schedule(add_experimental=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700697
698
699 def expect_result_gathering(self, job):
700 self.afe.get_jobs(id=job.id, finished=True).AndReturn(job)
701 entries = map(lambda s: s.entry, job.statuses)
702 self.afe.run('get_host_queue_entries',
703 job=job.id).AndReturn(entries)
704 if True not in map(lambda e: 'aborted' in e and e['aborted'], entries):
705 self.tko.get_status_counts(job=job.id).AndReturn(job.statuses)
706
707
Chris Masonefef21382012-01-17 11:16:32 -0800708 def _createSuiteWithMockedTestsAndControlFiles(self):
709 """Create a Suite, using mocked tests and control file contents.
710
711 @return Suite object, after mocking out behavior needed to create it.
712 """
Chris Masonefef21382012-01-17 11:16:32 -0800713 self.expect_control_file_parsing()
714 self.mox.ReplayAll()
Scott Zawalski9ece6532012-02-28 14:10:47 -0500715 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800716 self.getter, self.afe,
717 self.tko)
Chris Masonefef21382012-01-17 11:16:32 -0800718 self.mox.ResetAll()
719 return suite
720
721
Chris Masone6fed6462011-10-20 16:36:43 -0700722 def testWaitForResults(self):
723 """Should gather status and return records for job summaries."""
724 class FakeStatus(object):
725 """Fake replacement for server-side job status objects.
726
727 @var status: 'GOOD', 'FAIL', 'ERROR', etc.
728 @var test_name: name of the test this is status for
729 @var reason: reason for failure, if any
730 @var aborted: present and True if the job was aborted. Optional.
731 """
732 def __init__(self, code, name, reason, aborted=None):
733 self.status = code
734 self.test_name = name
735 self.reason = reason
736 self.entry = {}
Chris Masone99378582012-04-30 13:10:58 -0700737 self.test_started_time = '2012-11-11 11:11:11'
738 self.test_finished_time = '2012-11-11 12:12:12'
Chris Masone6fed6462011-10-20 16:36:43 -0700739 if aborted:
740 self.entry['aborted'] = True
741
Chris Masone99378582012-04-30 13:10:58 -0700742 def equals_record(self, status):
Chris Masone6fed6462011-10-20 16:36:43 -0700743 """Compares this object to a recorded status."""
Chris Masone99378582012-04-30 13:10:58 -0700744 return self._equals_record(status._status, status._test_name,
745 status._reason)
Chris Masone6fed6462011-10-20 16:36:43 -0700746
Chris Masone99378582012-04-30 13:10:58 -0700747 def _equals_record(self, status, name, reason=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700748 """Compares this object and fields of recorded status."""
749 if 'aborted' in self.entry and self.entry['aborted']:
750 return status == 'ABORT'
751 return (self.status == status and
752 self.test_name == name and
753 self.reason == reason)
754
Chris Masonefef21382012-01-17 11:16:32 -0800755 suite = self._createSuiteWithMockedTestsAndControlFiles()
Chris Masone6fed6462011-10-20 16:36:43 -0700756
757 jobs = [FakeJob(0, [FakeStatus('GOOD', 'T0', ''),
758 FakeStatus('GOOD', 'T1', '')]),
759 FakeJob(1, [FakeStatus('ERROR', 'T0', 'err', False),
760 FakeStatus('GOOD', 'T1', '')]),
761 FakeJob(2, [FakeStatus('TEST_NA', 'T0', 'no')]),
762 FakeJob(2, [FakeStatus('FAIL', 'T0', 'broken')]),
763 FakeJob(3, [FakeStatus('ERROR', 'T0', 'gah', True)])]
764 # To simulate a job that isn't ready the first time we check.
765 self.afe.get_jobs(id=jobs[0].id, finished=True).AndReturn([])
766 # Expect all the rest of the jobs to be good to go the first time.
767 for job in jobs[1:]:
768 self.expect_result_gathering(job)
769 # Then, expect job[0] to be ready.
770 self.expect_result_gathering(jobs[0])
771 # Expect us to poll twice.
772 self.mox.StubOutWithMock(time, 'sleep')
773 time.sleep(5)
774 time.sleep(5)
775 self.mox.ReplayAll()
776
Chris Masone6fed6462011-10-20 16:36:43 -0700777 suite._jobs = list(jobs)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500778 results = [result for result in suite.wait_for_results()]
Chris Masone6fed6462011-10-20 16:36:43 -0700779 for job in jobs:
780 for status in job.statuses:
781 self.assertTrue(True in map(status.equals_record, results))
782
783
Chris Masoneed356392012-05-08 14:07:13 -0700784 def schedule_and_expect_these_results(self, suite, results, recorder):
785 self.mox.StubOutWithMock(suite, 'schedule')
786 suite.schedule(True)
Chris Masone6fed6462011-10-20 16:36:43 -0700787 for result in results:
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500788 status = result[0]
Chris Masone99378582012-04-30 13:10:58 -0700789 test_name = result[1]
790 recorder.record_entry(
791 StatusContains.CreateFromStrings('START', test_name))
792 recorder.record_entry(
793 StatusContains.CreateFromStrings(*result)).InAnyOrder('results')
794 recorder.record_entry(
795 StatusContains.CreateFromStrings('END %s' % status, test_name))
Chris Masone6fed6462011-10-20 16:36:43 -0700796 self.mox.StubOutWithMock(suite, 'wait_for_results')
Chris Masone99378582012-04-30 13:10:58 -0700797 suite.wait_for_results().AndReturn(
798 map(lambda r: dynamic_suite.Status(*r), results))
Chris Masoneed356392012-05-08 14:07:13 -0700799
800
801 def testRunAndWaitSuccess(self):
802 """Should record successful results."""
803 suite = self._createSuiteWithMockedTestsAndControlFiles()
804
805 recorder = self.mox.CreateMock(base_job.base_job)
806 recorder.record_entry(
807 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
808
809 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
810 self.schedule_and_expect_these_results(suite, results, recorder)
811 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700812 self.mox.ReplayAll()
813
Chris Masone99378582012-04-30 13:10:58 -0700814 suite.run_and_wait(recorder.record_entry, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700815
816
817 def testRunAndWaitFailure(self):
818 """Should record failure to gather results."""
Chris Masonefef21382012-01-17 11:16:32 -0800819 suite = self._createSuiteWithMockedTestsAndControlFiles()
820
Chris Masone6fed6462011-10-20 16:36:43 -0700821 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700822 recorder.record_entry(
823 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
824 recorder.record_entry(
825 StatusContains.CreateFromStrings('FAIL', self._TAG, 'waiting'))
Chris Masone6fed6462011-10-20 16:36:43 -0700826
Chris Masone6fed6462011-10-20 16:36:43 -0700827 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone8b7cd422012-02-22 13:16:11 -0800828 suite.schedule(True)
Chris Masone6fed6462011-10-20 16:36:43 -0700829 self.mox.StubOutWithMock(suite, 'wait_for_results')
Chris Masone99378582012-04-30 13:10:58 -0700830 suite.wait_for_results().AndRaise(Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700831 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700832 self.mox.ReplayAll()
833
Chris Masone99378582012-04-30 13:10:58 -0700834 suite.run_and_wait(recorder.record_entry, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700835
836
837 def testRunAndWaitScheduleFailure(self):
Chris Masonefef21382012-01-17 11:16:32 -0800838 """Should record failure to schedule jobs."""
839 suite = self._createSuiteWithMockedTestsAndControlFiles()
840
Chris Masone6fed6462011-10-20 16:36:43 -0700841 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700842 recorder.record_entry(
843 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
844 recorder.record_entry(
845 StatusContains.CreateFromStrings('FAIL', self._TAG, 'scheduling'))
Chris Masone6fed6462011-10-20 16:36:43 -0700846
Chris Masone6fed6462011-10-20 16:36:43 -0700847 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone99378582012-04-30 13:10:58 -0700848 suite.schedule(True).AndRaise(Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700849 self.expect_control_file_reparsing()
850 self.mox.ReplayAll()
851
852 suite.run_and_wait(recorder.record_entry, True)
853
854
855 def testRunAndWaitDevServerRacyFailure(self):
856 """Should record discovery of dev server races in listing files."""
857 suite = self._createSuiteWithMockedTestsAndControlFiles()
858
859 recorder = self.mox.CreateMock(base_job.base_job)
860 recorder.record_entry(
861 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
862
863 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
864 self.schedule_and_expect_these_results(suite, results, recorder)
865
866 self.expect_racy_control_file_reparsing(
867 {'new': SuiteTest.FakeControlData('!')})
868
869 recorder.record_entry(
870 StatusContains.CreateFromStrings('FAIL', self._TAG, 'Dev Server'))
Chris Masone6fed6462011-10-20 16:36:43 -0700871 self.mox.ReplayAll()
872
Chris Masone99378582012-04-30 13:10:58 -0700873 suite.run_and_wait(recorder.record_entry, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700874
875
876if __name__ == '__main__':
877 unittest.main()