blob: 9a94763fd60efb7632dd13ff655341b26963fc5d [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 Masone5552dd72012-02-15 15:01:04 -080017from autotest_lib.client.common_lib import base_job, control_data, global_config
Chris Masone6fed6462011-10-20 16:36:43 -070018from autotest_lib.server.cros import control_file_getter, dynamic_suite
19from autotest_lib.server import frontend
20
21class FakeJob(object):
22 """Faked out RPC-client-side Job object."""
23 def __init__(self, id=0, statuses=[]):
24 self.id = id
Chris Masoned368cc42012-03-07 15:16:59 -080025 self.hostname = 'host%d' % id
Scott Zawalskie5bb1c52012-02-29 13:15:50 -050026 self.owner = 'tester'
Chris Masone6fed6462011-10-20 16:36:43 -070027 self.name = 'Fake Job %d' % self.id
28 self.statuses = statuses
29
30
Chris Masone5374c672012-03-05 15:11:39 -080031class FakeHost(object):
32 """Faked out RPC-client-side Host object."""
33 def __init__(self, status='Ready'):
34 self.status = status
35
Chris Masoned368cc42012-03-07 15:16:59 -080036class FakeLabel(object):
37 """Faked out RPC-client-side Label object."""
38 def __init__(self, id=0):
39 self.id = id
40
Chris Masone5374c672012-03-05 15:11:39 -080041
Chris Masoneab3e7332012-02-29 18:54:58 -080042class DynamicSuiteTest(mox.MoxTestBase):
43 """Unit tests for dynamic_suite module methods.
44
45 @var _DARGS: default args to vet.
46 """
47
48
49 def setUp(self):
50 super(DynamicSuiteTest, self).setUp()
51 self._DARGS = {'name': 'name',
52 'build': 'build',
53 'board': 'board',
54 'job': self.mox.CreateMock(base_job.base_job),
55 'num': 1,
56 'pool': 'pool',
57 'skip_reimage': True,
Chris Masone62579122012-03-08 15:18:43 -080058 'check_hosts': False,
Chris Masoneab3e7332012-02-29 18:54:58 -080059 'add_experimental': False}
60
61
62 def testVetRequiredReimageAndRunArgs(self):
63 """Should verify only that required args are present and correct."""
Chris Masone62579122012-03-08 15:18:43 -080064 build, board, name, job, _, _, _, _,_ = \
Chris Masoneab3e7332012-02-29 18:54:58 -080065 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
66 self.assertEquals(build, self._DARGS['build'])
67 self.assertEquals(board, self._DARGS['board'])
68 self.assertEquals(name, self._DARGS['name'])
69 self.assertEquals(job, self._DARGS['job'])
70
71
72 def testVetReimageAndRunBuildArgFail(self):
73 """Should fail verification because |build| arg is bad."""
74 self._DARGS['build'] = None
75 self.assertRaises(dynamic_suite.SuiteArgumentException,
76 dynamic_suite._vet_reimage_and_run_args,
77 **self._DARGS)
78
79
80 def testVetReimageAndRunBoardArgFail(self):
81 """Should fail verification because |board| arg is bad."""
82 self._DARGS['board'] = None
83 self.assertRaises(dynamic_suite.SuiteArgumentException,
84 dynamic_suite._vet_reimage_and_run_args,
85 **self._DARGS)
86
87
88 def testVetReimageAndRunNameArgFail(self):
89 """Should fail verification because |name| arg is bad."""
90 self._DARGS['name'] = None
91 self.assertRaises(dynamic_suite.SuiteArgumentException,
92 dynamic_suite._vet_reimage_and_run_args,
93 **self._DARGS)
94
95
96 def testVetReimageAndRunJobArgFail(self):
97 """Should fail verification because |job| arg is bad."""
98 self._DARGS['job'] = None
99 self.assertRaises(dynamic_suite.SuiteArgumentException,
100 dynamic_suite._vet_reimage_and_run_args,
101 **self._DARGS)
102
103
104 def testOverrideOptionalReimageAndRunArgs(self):
105 """Should verify that optional args can be overridden."""
Chris Masone62579122012-03-08 15:18:43 -0800106 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800107 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
108 self.assertEquals(pool, self._DARGS['pool'])
109 self.assertEquals(num, self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800110 self.assertEquals(check, self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800111 self.assertEquals(skip, self._DARGS['skip_reimage'])
112 self.assertEquals(expr, self._DARGS['add_experimental'])
113
114
115 def testDefaultOptionalReimageAndRunArgs(self):
116 """Should verify that optional args get defaults."""
117 del(self._DARGS['pool'])
118 del(self._DARGS['skip_reimage'])
Chris Masone62579122012-03-08 15:18:43 -0800119 del(self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800120 del(self._DARGS['add_experimental'])
121 del(self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800122 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800123 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
124 self.assertEquals(pool, None)
125 self.assertEquals(num, None)
Chris Masone62579122012-03-08 15:18:43 -0800126 self.assertEquals(check, True)
Chris Masoneab3e7332012-02-29 18:54:58 -0800127 self.assertEquals(skip, False)
128 self.assertEquals(expr, True)
129
130
Chris Masone6fed6462011-10-20 16:36:43 -0700131class ReimagerTest(mox.MoxTestBase):
132 """Unit tests for dynamic_suite.Reimager.
133
134 @var _URL: fake image url
Chris Masone8b7cd422012-02-22 13:16:11 -0800135 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700136 @var _NUM: fake number of machines to run on
137 @var _BOARD: fake board to reimage
138 """
139
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800140 _URL = 'http://nothing/%s'
Chris Masone8b7cd422012-02-22 13:16:11 -0800141 _BUILD = 'build'
Chris Masone6fed6462011-10-20 16:36:43 -0700142 _NUM = 4
143 _BOARD = 'board'
Chris Masone5552dd72012-02-15 15:01:04 -0800144 _CONFIG = global_config.global_config
Chris Masone6fed6462011-10-20 16:36:43 -0700145
146
147 def setUp(self):
148 super(ReimagerTest, self).setUp()
149 self.afe = self.mox.CreateMock(frontend.AFE)
150 self.tko = self.mox.CreateMock(frontend.TKO)
151 self.reimager = dynamic_suite.Reimager('', afe=self.afe, tko=self.tko)
Chris Masone5552dd72012-02-15 15:01:04 -0800152 self._CONFIG.override_config_value('CROS',
153 'sharding_factor',
154 "%d" % self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700155
156
157 def testEnsureVersionLabelAlreadyExists(self):
158 """Should not create a label if it already exists."""
159 name = 'label'
160 self.afe.get_labels(name=name).AndReturn([name])
161 self.mox.ReplayAll()
162 self.reimager._ensure_version_label(name)
163
164
165 def testEnsureVersionLabel(self):
166 """Should create a label if it doesn't already exist."""
167 name = 'label'
168 self.afe.get_labels(name=name).AndReturn([])
169 self.afe.create_label(name=name)
170 self.mox.ReplayAll()
171 self.reimager._ensure_version_label(name)
172
173
Chris Masone5374c672012-03-05 15:11:39 -0800174 def testCountHostsByBoardAndPool(self):
175 """Should count available hosts by board and pool."""
176 spec = [self._BOARD, 'pool:bvt']
177 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()])
178 self.mox.ReplayAll()
179 self.assertEquals(self.reimager._count_usable_hosts(spec), 1)
180
181
182 def testCountHostsByBoard(self):
183 """Should count available hosts by board."""
184 spec = [self._BOARD]
185 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()] * 2)
186 self.mox.ReplayAll()
187 self.assertEquals(self.reimager._count_usable_hosts(spec), 2)
188
189
190 def testCountZeroHostsByBoard(self):
191 """Should count the available hosts, by board, getting zero."""
192 spec = [self._BOARD]
193 self.afe.get_hosts(multiple_labels=spec).AndReturn([])
194 self.mox.ReplayAll()
195 self.assertEquals(self.reimager._count_usable_hosts(spec), 0)
196
197
Chris Masone6fed6462011-10-20 16:36:43 -0700198 def testInjectVars(self):
199 """Should inject dict of varibles into provided strings."""
200 def find_all_in(d, s):
201 """Returns true if all key-value pairs in |d| are printed in |s|."""
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800202 for k,v in d.iteritems():
203 if isinstance(v, str):
204 if "%s='%s'\n" % (k,v) not in s:
205 return False
206 else:
207 if "%s=%r\n" % (k,v) not in s:
208 return False
209 return True
Chris Masone6fed6462011-10-20 16:36:43 -0700210
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800211 v = {'v1': 'one', 'v2': 'two', 'v3': None, 'v4': False, 'v5': 5}
Chris Masone8b764252012-01-17 11:12:51 -0800212 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, '')))
213 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, 'ctrl')))
Chris Masone6fed6462011-10-20 16:36:43 -0700214
215
216 def testReportResultsGood(self):
217 """Should report results in the case where all jobs passed."""
218 job = self.mox.CreateMock(frontend.Job)
219 job.name = 'RPC Client job'
220 job.result = True
221 recorder = self.mox.CreateMock(base_job.base_job)
222 recorder.record('GOOD', mox.IgnoreArg(), job.name)
223 self.mox.ReplayAll()
224 self.reimager._report_results(job, recorder.record)
225
226
227 def testReportResultsBad(self):
228 """Should report results in various job failure cases.
229
230 In this test scenario, there are five hosts, all the 'netbook' platform.
231
232 h1: Did not run
233 h2: Two failed tests
234 h3: Two aborted tests
235 h4: completed, GOOD
236 h5: completed, GOOD
237 """
238 H1 = 'host1'
239 H2 = 'host2'
240 H3 = 'host3'
241 H4 = 'host4'
242 H5 = 'host5'
243
244 class FakeResult(object):
245 def __init__(self, reason):
246 self.reason = reason
247
248
249 # The RPC-client-side Job object that is annotated with results.
250 job = FakeJob()
251 job.result = None # job failed, there are results to report.
252
253 # The semantics of |results_platform_map| and |test_results| are
254 # drawn from frontend.AFE.poll_all_jobs()
255 job.results_platform_map = {'netbook': {'Aborted' : [H3],
Chris Masone8b7cd422012-02-22 13:16:11 -0800256 'Completed' : [H1, H4, H5],
257 'Failed': [H2]
Chris Masone6fed6462011-10-20 16:36:43 -0700258 }
259 }
260 # Gin up fake results for H2 and H3 failure cases.
261 h2 = frontend.TestResults()
262 h2.fail = [FakeResult('a'), FakeResult('b')]
263 h3 = frontend.TestResults()
264 h3.fail = [FakeResult('a'), FakeResult('b')]
265 # Skipping H1 in |test_status| dict means that it did not get run.
266 job.test_status = {H2: h2, H3: h3, H4: {}, H5: {}}
267
268 # Set up recording expectations.
269 rjob = self.mox.CreateMock(base_job.base_job)
270 for res in h2.fail:
271 rjob.record('FAIL', mox.IgnoreArg(), H2, res.reason).InAnyOrder()
272 for res in h3.fail:
273 rjob.record('ABORT', mox.IgnoreArg(), H3, res.reason).InAnyOrder()
274 rjob.record('GOOD', mox.IgnoreArg(), H4).InAnyOrder()
275 rjob.record('GOOD', mox.IgnoreArg(), H5).InAnyOrder()
276 rjob.record(
277 'ERROR', mox.IgnoreArg(), H1, mox.IgnoreArg()).InAnyOrder()
278
279 self.mox.ReplayAll()
280 self.reimager._report_results(job, rjob.record)
281
282
283 def testScheduleJob(self):
284 """Should be able to create a job with the AFE."""
285 # Fake out getting the autoupdate control file contents.
286 cf_getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
287 cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
288 self.reimager._cf_getter = cf_getter
289
Chris Masone6dca10a2012-02-15 15:41:42 -0800290 self._CONFIG.override_config_value('CROS',
291 'image_url_pattern',
292 self._URL)
Chris Masone6fed6462011-10-20 16:36:43 -0700293 self.afe.create_job(
Chris Masone8b7cd422012-02-22 13:16:11 -0800294 control_file=mox.And(mox.StrContains(self._BUILD),
295 mox.StrContains(self._URL % self._BUILD)),
296 name=mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700297 control_type='Server',
Chris Masone5374c672012-03-05 15:11:39 -0800298 meta_hosts=[self._BOARD] * self._NUM,
Chris Masone8b7cd422012-02-22 13:16:11 -0800299 dependencies=[])
Chris Masone6fed6462011-10-20 16:36:43 -0700300 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800301 self.reimager._schedule_reimage_job(self._BUILD, self._NUM, self._BOARD)
Chris Masone6fed6462011-10-20 16:36:43 -0700302
303
Chris Masoned368cc42012-03-07 15:16:59 -0800304 def expect_label_cleanup(self, build):
305 """Sets up |self.afe| to expect deletion of the version label.
306
307 @param build: the build the label is named after.
308 """
309 label = FakeLabel(id=random.randrange(0, 5))
310 self.afe.get_labels(
311 name__startswith=mox.StrContains(build)).AndReturn([label])
312 self.afe.run('delete_label', id=label.id)
313
314
Chris Masone62579122012-03-08 15:18:43 -0800315 def expect_attempt(self, success, ex=None, check_hosts=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700316 """Sets up |self.reimager| to expect an attempt() that returns |success|
317
Chris Masoned368cc42012-03-07 15:16:59 -0800318 Also stubs out Reimger._clear_build_state(), should the caller wish
319 to set an expectation there as well.
320
Chris Masone796fcf12012-02-22 16:53:31 -0800321 @param success: the value returned by poll_job_results()
322 @param ex: if not None, |ex| is raised by get_jobs()
Chris Masone6fed6462011-10-20 16:36:43 -0700323 @return a FakeJob configured with appropriate expectations
324 """
325 canary = FakeJob()
326 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
Chris Masone8b7cd422012-02-22 13:16:11 -0800327 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone6fed6462011-10-20 16:36:43 -0700328
329 self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
Chris Masone8b7cd422012-02-22 13:16:11 -0800330 self.reimager._schedule_reimage_job(self._BUILD,
Chris Masone6fed6462011-10-20 16:36:43 -0700331 self._NUM,
332 self._BOARD).AndReturn(canary)
Chris Masone62579122012-03-08 15:18:43 -0800333 if check_hosts:
334 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
335 self.reimager._count_usable_hosts(
336 mox.IgnoreArg()).AndReturn(self._NUM)
Chris Masone5374c672012-03-05 15:11:39 -0800337
Chris Masone6fed6462011-10-20 16:36:43 -0700338 if success is not None:
339 self.mox.StubOutWithMock(self.reimager, '_report_results')
340 self.reimager._report_results(canary, mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800341 canary.results_platform_map = {None: {'Total': [canary.hostname]}}
342
Chris Masone6fed6462011-10-20 16:36:43 -0700343
344 self.afe.get_jobs(id=canary.id, not_yet_run=True).AndReturn([])
Chris Masone796fcf12012-02-22 16:53:31 -0800345 if ex is not None:
346 self.afe.get_jobs(id=canary.id, finished=True).AndRaise(ex)
347 else:
348 self.afe.get_jobs(id=canary.id, finished=True).AndReturn([canary])
349 self.afe.poll_job_results(mox.IgnoreArg(),
350 canary, 0).AndReturn(success)
Chris Masone6fed6462011-10-20 16:36:43 -0700351
Chris Masoned368cc42012-03-07 15:16:59 -0800352 self.expect_label_cleanup(self._BUILD)
353 self.mox.StubOutWithMock(self.reimager, '_clear_build_state')
354
Chris Masone6fed6462011-10-20 16:36:43 -0700355 return canary
356
357
358 def testSuccessfulReimage(self):
359 """Should attempt a reimage and record success."""
Chris Masone62579122012-03-08 15:18:43 -0800360 canary = self.expect_attempt(success=True)
Chris Masone6fed6462011-10-20 16:36:43 -0700361
362 rjob = self.mox.CreateMock(base_job.base_job)
363 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
364 rjob.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800365 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
Chris Masone6fed6462011-10-20 16:36:43 -0700366 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800367 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800368 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700369
370
371 def testFailedReimage(self):
372 """Should attempt a reimage and record failure."""
Chris Masone62579122012-03-08 15:18:43 -0800373 canary = self.expect_attempt(success=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700374
375 rjob = self.mox.CreateMock(base_job.base_job)
376 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
377 rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800378 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
Chris Masone6fed6462011-10-20 16:36:43 -0700379 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800380 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800381 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700382
383
384 def testReimageThatNeverHappened(self):
385 """Should attempt a reimage and record that it didn't run."""
Chris Masone62579122012-03-08 15:18:43 -0800386 canary = self.expect_attempt(success=None)
Chris Masone6fed6462011-10-20 16:36:43 -0700387
388 rjob = self.mox.CreateMock(base_job.base_job)
389 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
390 rjob.record('FAIL', mox.IgnoreArg(), canary.name, mox.IgnoreArg())
391 rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
392 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800393 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800394 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700395
396
Chris Masone796fcf12012-02-22 16:53:31 -0800397 def testReimageThatRaised(self):
398 """Should attempt a reimage that raises an exception and record that."""
399 ex_message = 'Oh no!'
Chris Masone62579122012-03-08 15:18:43 -0800400 canary = self.expect_attempt(success=None, ex=Exception(ex_message))
Chris Masone796fcf12012-02-22 16:53:31 -0800401
402 rjob = self.mox.CreateMock(base_job.base_job)
403 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
404 rjob.record('END ERROR', mox.IgnoreArg(), mox.IgnoreArg(), ex_message)
405 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800406 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
407 self.reimager.clear_reimaged_host_state(self._BUILD)
408
409
410 def testSuccessfulReimageThatCouldNotScheduleRightAway(self):
411 """
412 Should attempt a reimage, ignoring host availability and record success.
413 """
414 canary = self.expect_attempt(success=True, check_hosts=False)
415
416 rjob = self.mox.CreateMock(base_job.base_job)
417 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
418 rjob.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
419 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
420 self.mox.ReplayAll()
421 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, False)
Chris Masoned368cc42012-03-07 15:16:59 -0800422 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone796fcf12012-02-22 16:53:31 -0800423
424
Chris Masone5374c672012-03-05 15:11:39 -0800425 def testReimageThatCouldNotSchedule(self):
426 """Should attempt a reimage that can't be scheduled."""
Chris Masone502b71e2012-04-10 10:41:35 -0700427 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
428 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
429
430 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
431 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(1)
432
433 rjob = self.mox.CreateMock(base_job.base_job)
434 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
435 rjob.record('END WARN', mox.IgnoreArg(), mox.IgnoreArg(),
436 mox.StrContains('Too few hosts'))
437 self.expect_label_cleanup(self._BUILD)
438 self.mox.ReplayAll()
439 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
440 self.reimager.clear_reimaged_host_state(self._BUILD)
441
442
443 def testReimageWithNoAvailableHosts(self):
444 """Should attempt a reimage while all hosts are dead."""
Chris Masone62579122012-03-08 15:18:43 -0800445 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
446 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone5374c672012-03-05 15:11:39 -0800447
448 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
449 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(0)
450
451 rjob = self.mox.CreateMock(base_job.base_job)
452 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
Chris Masone502b71e2012-04-10 10:41:35 -0700453 rjob.record('END ERROR', mox.IgnoreArg(), mox.IgnoreArg(),
454 mox.StrContains('All hosts'))
Chris Masoned368cc42012-03-07 15:16:59 -0800455 self.expect_label_cleanup(self._BUILD)
Chris Masone5374c672012-03-05 15:11:39 -0800456 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800457 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800458 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone5374c672012-03-05 15:11:39 -0800459
460
Chris Masone6fed6462011-10-20 16:36:43 -0700461class SuiteTest(mox.MoxTestBase):
462 """Unit tests for dynamic_suite.Suite.
463
Chris Masone8b7cd422012-02-22 13:16:11 -0800464 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700465 @var _TAG: fake suite tag
466 """
467
Chris Masone8b7cd422012-02-22 13:16:11 -0800468 _BUILD = 'build'
469 _TAG = 'suite_tag'
Chris Masone6fed6462011-10-20 16:36:43 -0700470
471
472 def setUp(self):
473 class FakeControlData(object):
474 """A fake parsed control file data structure."""
475 def __init__(self, data, expr=False):
476 self.string = 'text-' + data
477 self.name = 'name-' + data
478 self.data = data
479 self.test_type = 'Client'
480 self.experimental = expr
481
482
483 super(SuiteTest, self).setUp()
484 self.afe = self.mox.CreateMock(frontend.AFE)
485 self.tko = self.mox.CreateMock(frontend.TKO)
486
487 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__)
488
489 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
490
491 self.files = {'one': FakeControlData('data_one', expr=True),
492 'two': FakeControlData('data_two'),
493 'three': FakeControlData('data_three')}
494
495
496 def tearDown(self):
497 super(SuiteTest, self).tearDown()
498 shutil.rmtree(self.tmpdir, ignore_errors=True)
499
500
501 def expect_control_file_parsing(self):
502 """Expect an attempt to parse the 'control files' in |self.files|."""
503 self.getter.get_control_file_list().AndReturn(self.files.keys())
504 self.mox.StubOutWithMock(control_data, 'parse_control_string')
505 for file, data in self.files.iteritems():
506 self.getter.get_control_file_contents(file).AndReturn(data.string)
507 control_data.parse_control_string(
508 data.string, raise_warnings=True).AndReturn(data)
509
510
511 def testFindAndParseStableTests(self):
512 """Should find only non-experimental tests that match a predicate."""
513 self.expect_control_file_parsing()
514 self.mox.ReplayAll()
515
516 predicate = lambda d: d.text == self.files['two'].string
517 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter, predicate)
518 self.assertEquals(len(tests), 1)
519 self.assertEquals(tests[0], self.files['two'])
520
521
522 def testFindAndParseTests(self):
523 """Should find all tests that match a predicate."""
524 self.expect_control_file_parsing()
525 self.mox.ReplayAll()
526
527 predicate = lambda d: d.text != self.files['two'].string
528 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter,
529 predicate,
530 add_experimental=True)
531 self.assertEquals(len(tests), 2)
532 self.assertTrue(self.files['one'] in tests)
533 self.assertTrue(self.files['three'] in tests)
534
535
536 def mock_control_file_parsing(self):
537 """Fake out find_and_parse_tests(), returning content from |self.files|.
538 """
539 for test in self.files.values():
540 test.text = test.string # mimic parsing.
541 self.mox.StubOutWithMock(dynamic_suite.Suite, 'find_and_parse_tests')
542 dynamic_suite.Suite.find_and_parse_tests(
543 mox.IgnoreArg(),
544 mox.IgnoreArg(),
545 add_experimental=True).AndReturn(self.files.values())
546
547
548 def testStableUnstableFilter(self):
549 """Should distinguish between experimental and stable tests."""
550 self.mock_control_file_parsing()
551 self.mox.ReplayAll()
552 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
Chris Masoned6f38c82012-02-22 14:53:42 -0800553 afe=self.afe, tko=self.tko)
Chris Masone6fed6462011-10-20 16:36:43 -0700554
555 self.assertTrue(self.files['one'] in suite.tests)
556 self.assertTrue(self.files['two'] in suite.tests)
557 self.assertTrue(self.files['one'] in suite.unstable_tests())
558 self.assertTrue(self.files['two'] in suite.stable_tests())
559 self.assertFalse(self.files['one'] in suite.stable_tests())
560 self.assertFalse(self.files['two'] in suite.unstable_tests())
561
562
563 def expect_job_scheduling(self, add_experimental):
564 """Expect jobs to be scheduled for 'tests' in |self.files|.
565
566 @param add_experimental: expect jobs for experimental tests as well.
567 """
568 for test in self.files.values():
569 if not add_experimental and test.experimental:
570 continue
571 self.afe.create_job(
572 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800573 name=mox.And(mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700574 mox.StrContains(test.name)),
575 control_type=mox.IgnoreArg(),
Chris Masone8b7cd422012-02-22 13:16:11 -0800576 meta_hosts=[dynamic_suite.VERSION_PREFIX + self._BUILD],
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500577 dependencies=[]).AndReturn(FakeJob())
Chris Masone6fed6462011-10-20 16:36:43 -0700578
579
580 def testScheduleTests(self):
581 """Should schedule stable and experimental tests with the AFE."""
582 self.mock_control_file_parsing()
583 self.expect_job_scheduling(add_experimental=True)
584
585 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800586 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800587 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800588 suite.schedule()
Chris Masone6fed6462011-10-20 16:36:43 -0700589
590
Scott Zawalski9ece6532012-02-28 14:10:47 -0500591 def testScheduleTestsAndRecord(self):
592 """Should schedule stable and experimental tests with the AFE."""
593 self.mock_control_file_parsing()
594 self.mox.ReplayAll()
595 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
596 afe=self.afe, tko=self.tko,
597 results_dir=self.tmpdir)
598 self.mox.ResetAll()
599 self.expect_job_scheduling(add_experimental=True)
600 self.mox.StubOutWithMock(suite, '_record_scheduled_jobs')
601 suite._record_scheduled_jobs()
602 self.mox.ReplayAll()
603 suite.schedule()
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500604 for job in suite._jobs:
605 self.assertTrue(hasattr(job, 'test_name'))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500606
607
Chris Masone6fed6462011-10-20 16:36:43 -0700608 def testScheduleStableTests(self):
609 """Should schedule only stable tests with the AFE."""
610 self.mock_control_file_parsing()
611 self.expect_job_scheduling(add_experimental=False)
612
613 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800614 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800615 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800616 suite.schedule(add_experimental=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700617
618
619 def expect_result_gathering(self, job):
620 self.afe.get_jobs(id=job.id, finished=True).AndReturn(job)
621 entries = map(lambda s: s.entry, job.statuses)
622 self.afe.run('get_host_queue_entries',
623 job=job.id).AndReturn(entries)
624 if True not in map(lambda e: 'aborted' in e and e['aborted'], entries):
625 self.tko.get_status_counts(job=job.id).AndReturn(job.statuses)
626
627
Chris Masonefef21382012-01-17 11:16:32 -0800628 def _createSuiteWithMockedTestsAndControlFiles(self):
629 """Create a Suite, using mocked tests and control file contents.
630
631 @return Suite object, after mocking out behavior needed to create it.
632 """
Chris Masonefef21382012-01-17 11:16:32 -0800633 self.expect_control_file_parsing()
634 self.mox.ReplayAll()
Scott Zawalski9ece6532012-02-28 14:10:47 -0500635 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800636 self.getter, self.afe,
637 self.tko)
Chris Masonefef21382012-01-17 11:16:32 -0800638 self.mox.ResetAll()
639 return suite
640
641
Chris Masone6fed6462011-10-20 16:36:43 -0700642 def testWaitForResults(self):
643 """Should gather status and return records for job summaries."""
644 class FakeStatus(object):
645 """Fake replacement for server-side job status objects.
646
647 @var status: 'GOOD', 'FAIL', 'ERROR', etc.
648 @var test_name: name of the test this is status for
649 @var reason: reason for failure, if any
650 @var aborted: present and True if the job was aborted. Optional.
651 """
652 def __init__(self, code, name, reason, aborted=None):
653 self.status = code
654 self.test_name = name
655 self.reason = reason
656 self.entry = {}
657 if aborted:
658 self.entry['aborted'] = True
659
660 def equals_record(self, args):
661 """Compares this object to a recorded status."""
662 return self._equals_record(*args)
663
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800664 def _equals_record(self, status, subdir, name, reason=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700665 """Compares this object and fields of recorded status."""
666 if 'aborted' in self.entry and self.entry['aborted']:
667 return status == 'ABORT'
668 return (self.status == status and
669 self.test_name == name and
670 self.reason == reason)
671
Chris Masonefef21382012-01-17 11:16:32 -0800672 suite = self._createSuiteWithMockedTestsAndControlFiles()
Chris Masone6fed6462011-10-20 16:36:43 -0700673
674 jobs = [FakeJob(0, [FakeStatus('GOOD', 'T0', ''),
675 FakeStatus('GOOD', 'T1', '')]),
676 FakeJob(1, [FakeStatus('ERROR', 'T0', 'err', False),
677 FakeStatus('GOOD', 'T1', '')]),
678 FakeJob(2, [FakeStatus('TEST_NA', 'T0', 'no')]),
679 FakeJob(2, [FakeStatus('FAIL', 'T0', 'broken')]),
680 FakeJob(3, [FakeStatus('ERROR', 'T0', 'gah', True)])]
681 # To simulate a job that isn't ready the first time we check.
682 self.afe.get_jobs(id=jobs[0].id, finished=True).AndReturn([])
683 # Expect all the rest of the jobs to be good to go the first time.
684 for job in jobs[1:]:
685 self.expect_result_gathering(job)
686 # Then, expect job[0] to be ready.
687 self.expect_result_gathering(jobs[0])
688 # Expect us to poll twice.
689 self.mox.StubOutWithMock(time, 'sleep')
690 time.sleep(5)
691 time.sleep(5)
692 self.mox.ReplayAll()
693
Chris Masone6fed6462011-10-20 16:36:43 -0700694 suite._jobs = list(jobs)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500695 results = [result for result in suite.wait_for_results()]
Chris Masone6fed6462011-10-20 16:36:43 -0700696 for job in jobs:
697 for status in job.statuses:
698 self.assertTrue(True in map(status.equals_record, results))
699
700
701 def testRunAndWaitSuccess(self):
702 """Should record successful results."""
Chris Masonefef21382012-01-17 11:16:32 -0800703 suite = self._createSuiteWithMockedTestsAndControlFiles()
Chris Masone6fed6462011-10-20 16:36:43 -0700704
Chris Masonefef21382012-01-17 11:16:32 -0800705 results = [('GOOD', None, 'good'), ('FAIL', None, 'bad', 'reason')]
Chris Masone6fed6462011-10-20 16:36:43 -0700706 recorder = self.mox.CreateMock(base_job.base_job)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500707 recorder.record('INFO', None, 'Start %s' % self._TAG)
Chris Masone6fed6462011-10-20 16:36:43 -0700708 for result in results:
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500709 status = result[0]
710 test_name = result[2]
711 recorder.record('START', None, test_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700712 recorder.record(*result).InAnyOrder('results')
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500713 recorder.record('END %s' % status, None, test_name)
Chris Masone6fed6462011-10-20 16:36:43 -0700714
Chris Masone6fed6462011-10-20 16:36:43 -0700715 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone8b7cd422012-02-22 13:16:11 -0800716 suite.schedule(True)
Chris Masone6fed6462011-10-20 16:36:43 -0700717 self.mox.StubOutWithMock(suite, 'wait_for_results')
718 suite.wait_for_results().AndReturn(results)
719 self.mox.ReplayAll()
720
Chris Masone8b7cd422012-02-22 13:16:11 -0800721 suite.run_and_wait(recorder.record, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700722
723
724 def testRunAndWaitFailure(self):
725 """Should record failure to gather results."""
Chris Masonefef21382012-01-17 11:16:32 -0800726 suite = self._createSuiteWithMockedTestsAndControlFiles()
727
Chris Masone6fed6462011-10-20 16:36:43 -0700728 recorder = self.mox.CreateMock(base_job.base_job)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500729 recorder.record('INFO', None, 'Start %s' % self._TAG)
730 recorder.record('FAIL', None, self._TAG,
731 mox.StrContains('waiting'))
Chris Masone6fed6462011-10-20 16:36:43 -0700732
Chris Masone6fed6462011-10-20 16:36:43 -0700733 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone8b7cd422012-02-22 13:16:11 -0800734 suite.schedule(True)
Chris Masone6fed6462011-10-20 16:36:43 -0700735 self.mox.StubOutWithMock(suite, 'wait_for_results')
736 suite.wait_for_results().AndRaise(Exception())
737 self.mox.ReplayAll()
738
Chris Masone8b7cd422012-02-22 13:16:11 -0800739 suite.run_and_wait(recorder.record, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700740
741
742 def testRunAndWaitScheduleFailure(self):
Chris Masonefef21382012-01-17 11:16:32 -0800743 """Should record failure to schedule jobs."""
744 suite = self._createSuiteWithMockedTestsAndControlFiles()
745
Chris Masone6fed6462011-10-20 16:36:43 -0700746 recorder = self.mox.CreateMock(base_job.base_job)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500747 recorder.record('INFO', None, 'Start %s' % self._TAG)
748 recorder.record('FAIL', None, self._TAG,
749 mox.StrContains('scheduling'))
Chris Masone6fed6462011-10-20 16:36:43 -0700750
Chris Masone6fed6462011-10-20 16:36:43 -0700751 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone8b7cd422012-02-22 13:16:11 -0800752 suite.schedule(True).AndRaise(Exception())
Chris Masone6fed6462011-10-20 16:36:43 -0700753 self.mox.ReplayAll()
754
Chris Masone8b7cd422012-02-22 13:16:11 -0800755 suite.run_and_wait(recorder.record, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700756
757
758if __name__ == '__main__':
759 unittest.main()