blob: 4553d281e101eea8ad9766265d5a7475439b014f [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 Masone47c9e642012-04-25 14:22:18 -070018from autotest_lib.frontend.afe.json_rpc import proxy
Chris Masone6fed6462011-10-20 16:36:43 -070019from autotest_lib.server.cros import control_file_getter, dynamic_suite
20from autotest_lib.server import frontend
21
22class FakeJob(object):
23 """Faked out RPC-client-side Job object."""
24 def __init__(self, id=0, statuses=[]):
25 self.id = id
Chris Masoned368cc42012-03-07 15:16:59 -080026 self.hostname = 'host%d' % id
Scott Zawalskie5bb1c52012-02-29 13:15:50 -050027 self.owner = 'tester'
Chris Masone6fed6462011-10-20 16:36:43 -070028 self.name = 'Fake Job %d' % self.id
29 self.statuses = statuses
30
31
Chris Masone5374c672012-03-05 15:11:39 -080032class FakeHost(object):
33 """Faked out RPC-client-side Host object."""
34 def __init__(self, status='Ready'):
35 self.status = status
36
Chris Masoned368cc42012-03-07 15:16:59 -080037class FakeLabel(object):
38 """Faked out RPC-client-side Label object."""
39 def __init__(self, id=0):
40 self.id = id
41
Chris Masone5374c672012-03-05 15:11:39 -080042
Chris Masoneab3e7332012-02-29 18:54:58 -080043class DynamicSuiteTest(mox.MoxTestBase):
44 """Unit tests for dynamic_suite module methods.
45
46 @var _DARGS: default args to vet.
47 """
48
49
50 def setUp(self):
51 super(DynamicSuiteTest, self).setUp()
52 self._DARGS = {'name': 'name',
53 'build': 'build',
54 'board': 'board',
55 'job': self.mox.CreateMock(base_job.base_job),
56 'num': 1,
57 'pool': 'pool',
58 'skip_reimage': True,
Chris Masone62579122012-03-08 15:18:43 -080059 'check_hosts': False,
Chris Masoneab3e7332012-02-29 18:54:58 -080060 'add_experimental': False}
61
62
63 def testVetRequiredReimageAndRunArgs(self):
64 """Should verify only that required args are present and correct."""
Chris Masone62579122012-03-08 15:18:43 -080065 build, board, name, job, _, _, _, _,_ = \
Chris Masoneab3e7332012-02-29 18:54:58 -080066 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
67 self.assertEquals(build, self._DARGS['build'])
68 self.assertEquals(board, self._DARGS['board'])
69 self.assertEquals(name, self._DARGS['name'])
70 self.assertEquals(job, self._DARGS['job'])
71
72
73 def testVetReimageAndRunBuildArgFail(self):
74 """Should fail verification because |build| arg is bad."""
75 self._DARGS['build'] = None
76 self.assertRaises(dynamic_suite.SuiteArgumentException,
77 dynamic_suite._vet_reimage_and_run_args,
78 **self._DARGS)
79
80
81 def testVetReimageAndRunBoardArgFail(self):
82 """Should fail verification because |board| arg is bad."""
83 self._DARGS['board'] = None
84 self.assertRaises(dynamic_suite.SuiteArgumentException,
85 dynamic_suite._vet_reimage_and_run_args,
86 **self._DARGS)
87
88
89 def testVetReimageAndRunNameArgFail(self):
90 """Should fail verification because |name| arg is bad."""
91 self._DARGS['name'] = None
92 self.assertRaises(dynamic_suite.SuiteArgumentException,
93 dynamic_suite._vet_reimage_and_run_args,
94 **self._DARGS)
95
96
97 def testVetReimageAndRunJobArgFail(self):
98 """Should fail verification because |job| arg is bad."""
99 self._DARGS['job'] = None
100 self.assertRaises(dynamic_suite.SuiteArgumentException,
101 dynamic_suite._vet_reimage_and_run_args,
102 **self._DARGS)
103
104
105 def testOverrideOptionalReimageAndRunArgs(self):
106 """Should verify that optional args can be overridden."""
Chris Masone62579122012-03-08 15:18:43 -0800107 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800108 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
109 self.assertEquals(pool, self._DARGS['pool'])
110 self.assertEquals(num, self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800111 self.assertEquals(check, self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800112 self.assertEquals(skip, self._DARGS['skip_reimage'])
113 self.assertEquals(expr, self._DARGS['add_experimental'])
114
115
116 def testDefaultOptionalReimageAndRunArgs(self):
117 """Should verify that optional args get defaults."""
118 del(self._DARGS['pool'])
119 del(self._DARGS['skip_reimage'])
Chris Masone62579122012-03-08 15:18:43 -0800120 del(self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800121 del(self._DARGS['add_experimental'])
122 del(self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800123 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800124 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
125 self.assertEquals(pool, None)
126 self.assertEquals(num, None)
Chris Masone62579122012-03-08 15:18:43 -0800127 self.assertEquals(check, True)
Chris Masoneab3e7332012-02-29 18:54:58 -0800128 self.assertEquals(skip, False)
129 self.assertEquals(expr, True)
130
131
Chris Masone6fed6462011-10-20 16:36:43 -0700132class ReimagerTest(mox.MoxTestBase):
133 """Unit tests for dynamic_suite.Reimager.
134
135 @var _URL: fake image url
Chris Masone8b7cd422012-02-22 13:16:11 -0800136 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700137 @var _NUM: fake number of machines to run on
138 @var _BOARD: fake board to reimage
139 """
140
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800141 _URL = 'http://nothing/%s'
Chris Masone8b7cd422012-02-22 13:16:11 -0800142 _BUILD = 'build'
Chris Masone6fed6462011-10-20 16:36:43 -0700143 _NUM = 4
144 _BOARD = 'board'
Chris Masone5552dd72012-02-15 15:01:04 -0800145 _CONFIG = global_config.global_config
Chris Masone6fed6462011-10-20 16:36:43 -0700146
147
148 def setUp(self):
149 super(ReimagerTest, self).setUp()
150 self.afe = self.mox.CreateMock(frontend.AFE)
151 self.tko = self.mox.CreateMock(frontend.TKO)
152 self.reimager = dynamic_suite.Reimager('', afe=self.afe, tko=self.tko)
Chris Masone5552dd72012-02-15 15:01:04 -0800153 self._CONFIG.override_config_value('CROS',
154 'sharding_factor',
155 "%d" % self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700156
157
158 def testEnsureVersionLabelAlreadyExists(self):
Chris Masone47c9e642012-04-25 14:22:18 -0700159 """Should tolerate a label that already exists."""
Chris Masone6fed6462011-10-20 16:36:43 -0700160 name = 'label'
Chris Masone47c9e642012-04-25 14:22:18 -0700161 error = proxy.ValidationError(
162 {'name': 'ValidationError',
163 'message': '{"name": "This value must be unique"}',
164 'traceback': ''},
165 'BAD')
166 self.afe.create_label(name=name).AndRaise(error)
Chris Masone6fed6462011-10-20 16:36:43 -0700167 self.mox.ReplayAll()
168 self.reimager._ensure_version_label(name)
169
170
171 def testEnsureVersionLabel(self):
172 """Should create a label if it doesn't already exist."""
173 name = 'label'
Chris Masone6fed6462011-10-20 16:36:43 -0700174 self.afe.create_label(name=name)
175 self.mox.ReplayAll()
176 self.reimager._ensure_version_label(name)
177
178
Chris Masone5374c672012-03-05 15:11:39 -0800179 def testCountHostsByBoardAndPool(self):
180 """Should count available hosts by board and pool."""
181 spec = [self._BOARD, 'pool:bvt']
182 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()])
183 self.mox.ReplayAll()
184 self.assertEquals(self.reimager._count_usable_hosts(spec), 1)
185
186
187 def testCountHostsByBoard(self):
188 """Should count available hosts by board."""
189 spec = [self._BOARD]
190 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()] * 2)
191 self.mox.ReplayAll()
192 self.assertEquals(self.reimager._count_usable_hosts(spec), 2)
193
194
195 def testCountZeroHostsByBoard(self):
196 """Should count the available hosts, by board, getting zero."""
197 spec = [self._BOARD]
198 self.afe.get_hosts(multiple_labels=spec).AndReturn([])
199 self.mox.ReplayAll()
200 self.assertEquals(self.reimager._count_usable_hosts(spec), 0)
201
202
Chris Masone6fed6462011-10-20 16:36:43 -0700203 def testInjectVars(self):
204 """Should inject dict of varibles into provided strings."""
205 def find_all_in(d, s):
206 """Returns true if all key-value pairs in |d| are printed in |s|."""
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800207 for k,v in d.iteritems():
208 if isinstance(v, str):
209 if "%s='%s'\n" % (k,v) not in s:
210 return False
211 else:
212 if "%s=%r\n" % (k,v) not in s:
213 return False
214 return True
Chris Masone6fed6462011-10-20 16:36:43 -0700215
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800216 v = {'v1': 'one', 'v2': 'two', 'v3': None, 'v4': False, 'v5': 5}
Chris Masone8b764252012-01-17 11:12:51 -0800217 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, '')))
218 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, 'ctrl')))
Chris Masone6fed6462011-10-20 16:36:43 -0700219
220
221 def testReportResultsGood(self):
222 """Should report results in the case where all jobs passed."""
223 job = self.mox.CreateMock(frontend.Job)
224 job.name = 'RPC Client job'
225 job.result = True
226 recorder = self.mox.CreateMock(base_job.base_job)
227 recorder.record('GOOD', mox.IgnoreArg(), job.name)
228 self.mox.ReplayAll()
229 self.reimager._report_results(job, recorder.record)
230
231
232 def testReportResultsBad(self):
233 """Should report results in various job failure cases.
234
235 In this test scenario, there are five hosts, all the 'netbook' platform.
236
237 h1: Did not run
238 h2: Two failed tests
239 h3: Two aborted tests
240 h4: completed, GOOD
241 h5: completed, GOOD
242 """
243 H1 = 'host1'
244 H2 = 'host2'
245 H3 = 'host3'
246 H4 = 'host4'
247 H5 = 'host5'
248
249 class FakeResult(object):
250 def __init__(self, reason):
251 self.reason = reason
252
253
254 # The RPC-client-side Job object that is annotated with results.
255 job = FakeJob()
256 job.result = None # job failed, there are results to report.
257
258 # The semantics of |results_platform_map| and |test_results| are
259 # drawn from frontend.AFE.poll_all_jobs()
260 job.results_platform_map = {'netbook': {'Aborted' : [H3],
Chris Masone8b7cd422012-02-22 13:16:11 -0800261 'Completed' : [H1, H4, H5],
262 'Failed': [H2]
Chris Masone6fed6462011-10-20 16:36:43 -0700263 }
264 }
265 # Gin up fake results for H2 and H3 failure cases.
266 h2 = frontend.TestResults()
267 h2.fail = [FakeResult('a'), FakeResult('b')]
268 h3 = frontend.TestResults()
269 h3.fail = [FakeResult('a'), FakeResult('b')]
270 # Skipping H1 in |test_status| dict means that it did not get run.
271 job.test_status = {H2: h2, H3: h3, H4: {}, H5: {}}
272
273 # Set up recording expectations.
274 rjob = self.mox.CreateMock(base_job.base_job)
275 for res in h2.fail:
276 rjob.record('FAIL', mox.IgnoreArg(), H2, res.reason).InAnyOrder()
277 for res in h3.fail:
278 rjob.record('ABORT', mox.IgnoreArg(), H3, res.reason).InAnyOrder()
279 rjob.record('GOOD', mox.IgnoreArg(), H4).InAnyOrder()
280 rjob.record('GOOD', mox.IgnoreArg(), H5).InAnyOrder()
281 rjob.record(
282 'ERROR', mox.IgnoreArg(), H1, mox.IgnoreArg()).InAnyOrder()
283
284 self.mox.ReplayAll()
285 self.reimager._report_results(job, rjob.record)
286
287
288 def testScheduleJob(self):
289 """Should be able to create a job with the AFE."""
290 # Fake out getting the autoupdate control file contents.
291 cf_getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
292 cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
293 self.reimager._cf_getter = cf_getter
294
Chris Masone6dca10a2012-02-15 15:41:42 -0800295 self._CONFIG.override_config_value('CROS',
296 'image_url_pattern',
297 self._URL)
Chris Masone6fed6462011-10-20 16:36:43 -0700298 self.afe.create_job(
Chris Masone8b7cd422012-02-22 13:16:11 -0800299 control_file=mox.And(mox.StrContains(self._BUILD),
300 mox.StrContains(self._URL % self._BUILD)),
301 name=mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700302 control_type='Server',
Chris Masone5374c672012-03-05 15:11:39 -0800303 meta_hosts=[self._BOARD] * self._NUM,
Chris Masone99378582012-04-30 13:10:58 -0700304 dependencies=[],
305 priority='Low')
Chris Masone6fed6462011-10-20 16:36:43 -0700306 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800307 self.reimager._schedule_reimage_job(self._BUILD, self._NUM, self._BOARD)
Chris Masone6fed6462011-10-20 16:36:43 -0700308
309
Chris Masone62579122012-03-08 15:18:43 -0800310 def expect_attempt(self, success, ex=None, check_hosts=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700311 """Sets up |self.reimager| to expect an attempt() that returns |success|
312
Chris Masoned368cc42012-03-07 15:16:59 -0800313 Also stubs out Reimger._clear_build_state(), should the caller wish
314 to set an expectation there as well.
315
Chris Masone796fcf12012-02-22 16:53:31 -0800316 @param success: the value returned by poll_job_results()
317 @param ex: if not None, |ex| is raised by get_jobs()
Chris Masone6fed6462011-10-20 16:36:43 -0700318 @return a FakeJob configured with appropriate expectations
319 """
320 canary = FakeJob()
321 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
Chris Masone8b7cd422012-02-22 13:16:11 -0800322 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone6fed6462011-10-20 16:36:43 -0700323
324 self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
Chris Masone8b7cd422012-02-22 13:16:11 -0800325 self.reimager._schedule_reimage_job(self._BUILD,
Chris Masone6fed6462011-10-20 16:36:43 -0700326 self._NUM,
327 self._BOARD).AndReturn(canary)
Chris Masone62579122012-03-08 15:18:43 -0800328 if check_hosts:
329 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
330 self.reimager._count_usable_hosts(
331 mox.IgnoreArg()).AndReturn(self._NUM)
Chris Masone5374c672012-03-05 15:11:39 -0800332
Chris Masone6fed6462011-10-20 16:36:43 -0700333 if success is not None:
334 self.mox.StubOutWithMock(self.reimager, '_report_results')
335 self.reimager._report_results(canary, mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800336 canary.results_platform_map = {None: {'Total': [canary.hostname]}}
337
Chris Masone6fed6462011-10-20 16:36:43 -0700338
339 self.afe.get_jobs(id=canary.id, not_yet_run=True).AndReturn([])
Chris Masone796fcf12012-02-22 16:53:31 -0800340 if ex is not None:
341 self.afe.get_jobs(id=canary.id, finished=True).AndRaise(ex)
342 else:
343 self.afe.get_jobs(id=canary.id, finished=True).AndReturn([canary])
344 self.afe.poll_job_results(mox.IgnoreArg(),
345 canary, 0).AndReturn(success)
Chris Masone6fed6462011-10-20 16:36:43 -0700346
Chris Masoned368cc42012-03-07 15:16:59 -0800347 self.mox.StubOutWithMock(self.reimager, '_clear_build_state')
348
Chris Masone6fed6462011-10-20 16:36:43 -0700349 return canary
350
351
352 def testSuccessfulReimage(self):
353 """Should attempt a reimage and record success."""
Chris Masone62579122012-03-08 15:18:43 -0800354 canary = self.expect_attempt(success=True)
Chris Masone6fed6462011-10-20 16:36:43 -0700355
356 rjob = self.mox.CreateMock(base_job.base_job)
357 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
358 rjob.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800359 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
Chris Masone6fed6462011-10-20 16:36:43 -0700360 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800361 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800362 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700363
364
365 def testFailedReimage(self):
366 """Should attempt a reimage and record failure."""
Chris Masone62579122012-03-08 15:18:43 -0800367 canary = self.expect_attempt(success=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700368
369 rjob = self.mox.CreateMock(base_job.base_job)
370 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
371 rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800372 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
Chris Masone6fed6462011-10-20 16:36:43 -0700373 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800374 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800375 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700376
377
378 def testReimageThatNeverHappened(self):
379 """Should attempt a reimage and record that it didn't run."""
Chris Masone62579122012-03-08 15:18:43 -0800380 canary = self.expect_attempt(success=None)
Chris Masone6fed6462011-10-20 16:36:43 -0700381
382 rjob = self.mox.CreateMock(base_job.base_job)
383 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
384 rjob.record('FAIL', mox.IgnoreArg(), canary.name, mox.IgnoreArg())
385 rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
386 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800387 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800388 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700389
390
Chris Masone796fcf12012-02-22 16:53:31 -0800391 def testReimageThatRaised(self):
392 """Should attempt a reimage that raises an exception and record that."""
393 ex_message = 'Oh no!'
Chris Masone62579122012-03-08 15:18:43 -0800394 canary = self.expect_attempt(success=None, ex=Exception(ex_message))
Chris Masone796fcf12012-02-22 16:53:31 -0800395
396 rjob = self.mox.CreateMock(base_job.base_job)
397 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
398 rjob.record('END ERROR', mox.IgnoreArg(), mox.IgnoreArg(), ex_message)
399 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800400 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
401 self.reimager.clear_reimaged_host_state(self._BUILD)
402
403
404 def testSuccessfulReimageThatCouldNotScheduleRightAway(self):
Chris Masone99378582012-04-30 13:10:58 -0700405 """Should attempt reimage, ignoring host availability; record success.
Chris Masone62579122012-03-08 15:18:43 -0800406 """
407 canary = self.expect_attempt(success=True, check_hosts=False)
408
409 rjob = self.mox.CreateMock(base_job.base_job)
410 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
411 rjob.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
412 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
413 self.mox.ReplayAll()
414 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, False)
Chris Masoned368cc42012-03-07 15:16:59 -0800415 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone796fcf12012-02-22 16:53:31 -0800416
417
Chris Masone5374c672012-03-05 15:11:39 -0800418 def testReimageThatCouldNotSchedule(self):
419 """Should attempt a reimage that can't be scheduled."""
Chris Masone502b71e2012-04-10 10:41:35 -0700420 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
421 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
422
423 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
424 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(1)
425
426 rjob = self.mox.CreateMock(base_job.base_job)
427 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
428 rjob.record('END WARN', mox.IgnoreArg(), mox.IgnoreArg(),
429 mox.StrContains('Too few hosts'))
Chris Masone502b71e2012-04-10 10:41:35 -0700430 self.mox.ReplayAll()
431 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
432 self.reimager.clear_reimaged_host_state(self._BUILD)
433
434
435 def testReimageWithNoAvailableHosts(self):
436 """Should attempt a reimage while all hosts are dead."""
Chris Masone62579122012-03-08 15:18:43 -0800437 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
438 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone5374c672012-03-05 15:11:39 -0800439
440 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
441 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(0)
442
443 rjob = self.mox.CreateMock(base_job.base_job)
444 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
Chris Masone502b71e2012-04-10 10:41:35 -0700445 rjob.record('END ERROR', mox.IgnoreArg(), mox.IgnoreArg(),
446 mox.StrContains('All hosts'))
Chris Masone5374c672012-03-05 15:11:39 -0800447 self.mox.ReplayAll()
Chris Masone62579122012-03-08 15:18:43 -0800448 self.reimager.attempt(self._BUILD, self._BOARD, rjob.record, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800449 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone5374c672012-03-05 15:11:39 -0800450
451
Chris Masone99378582012-04-30 13:10:58 -0700452class StatusContains(mox.Comparator):
453 @staticmethod
454 def CreateFromStrings(status=None, test_name=None, reason=None):
455 status_comp = mox.StrContains(status) if status else mox.IgnoreArg()
456 name_comp = mox.StrContains(test_name) if test_name else mox.IgnoreArg()
457 reason_comp = mox.StrContains(reason) if reason else mox.IgnoreArg()
458 return StatusContains(status_comp, name_comp, reason_comp)
459
460
461 def __init__(self, status=mox.IgnoreArg(), test_name=mox.IgnoreArg(),
462 reason=mox.IgnoreArg()):
463 """Initialize.
464
465 Takes mox.Comparator objects to apply to dynamic_suite.Status
466 member variables.
467
468 @param status: status code, e.g. 'INFO', 'START', etc.
469 @param test_name: expected test name.
470 @param reason: expected reason
471 """
472 self._status = status
473 self._test_name = test_name
474 self._reason = reason
475
476
477 def equals(self, rhs):
478 """Check to see if fields match dynamic_suite.Status obj in rhs.
479
480 @param rhs: dynamic_suite.Status object to match.
481 @return boolean
482 """
483 return (self._status.equals(rhs.status_code) and
484 self._test_name.equals(rhs.operation) and
485 self._reason.equals(rhs.message))
486
487
488 def __repr__(self):
489 return '<Status containing \'%s\t%s\t%s\'>' % (self._status,
490 self._test_name,
491 self._reason)
492
493
Chris Masone6fed6462011-10-20 16:36:43 -0700494class SuiteTest(mox.MoxTestBase):
495 """Unit tests for dynamic_suite.Suite.
496
Chris Masone8b7cd422012-02-22 13:16:11 -0800497 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700498 @var _TAG: fake suite tag
499 """
500
Chris Masone8b7cd422012-02-22 13:16:11 -0800501 _BUILD = 'build'
502 _TAG = 'suite_tag'
Chris Masone6fed6462011-10-20 16:36:43 -0700503
504
Chris Masoneed356392012-05-08 14:07:13 -0700505 class FakeControlData(object):
506 """A fake parsed control file data structure."""
507 def __init__(self, data, expr=False):
508 self.string = 'text-' + data
509 self.name = 'name-' + data
510 self.data = data
511 self.suite = SuiteTest._TAG
512 self.test_type = 'Client'
513 self.experimental = expr
514
515
Chris Masone6fed6462011-10-20 16:36:43 -0700516 def setUp(self):
Chris Masone6fed6462011-10-20 16:36:43 -0700517 super(SuiteTest, self).setUp()
518 self.afe = self.mox.CreateMock(frontend.AFE)
519 self.tko = self.mox.CreateMock(frontend.TKO)
520
521 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__)
522
523 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
524
Chris Masoneed356392012-05-08 14:07:13 -0700525 self.files = {'one': SuiteTest.FakeControlData('data_one', expr=True),
526 'two': SuiteTest.FakeControlData('data_two'),
527 'three': SuiteTest.FakeControlData('data_three')}
Chris Masone6fed6462011-10-20 16:36:43 -0700528
Chris Masone75a20612012-05-08 12:37:31 -0700529 self.files_to_filter = {
Chris Masoneed356392012-05-08 14:07:13 -0700530 'with/deps/...': SuiteTest.FakeControlData('...gets filtered'),
531 'with/profilers/...': SuiteTest.FakeControlData('...gets filtered')}
Chris Masone75a20612012-05-08 12:37:31 -0700532
Chris Masone6fed6462011-10-20 16:36:43 -0700533
534 def tearDown(self):
535 super(SuiteTest, self).tearDown()
536 shutil.rmtree(self.tmpdir, ignore_errors=True)
537
538
539 def expect_control_file_parsing(self):
540 """Expect an attempt to parse the 'control files' in |self.files|."""
Chris Masone75a20612012-05-08 12:37:31 -0700541 all_files = self.files.keys() + self.files_to_filter.keys()
Chris Masoneed356392012-05-08 14:07:13 -0700542 self._set_control_file_parsing_expectations(False, all_files,
543 self.files.iteritems())
544
545
546 def expect_control_file_reparsing(self):
547 """Expect re-parsing the 'control files' in |self.files|."""
548 all_files = self.files.keys() + self.files_to_filter.keys()
549 self._set_control_file_parsing_expectations(True, all_files,
550 self.files.iteritems())
551
552
553 def expect_racy_control_file_reparsing(self, new_files):
554 """Expect re-fetching and parsing of control files to return extra.
555
556 @param new_files: extra control files that showed up during scheduling.
557 """
558 all_files = (self.files.keys() + self.files_to_filter.keys() +
559 new_files.keys())
560 new_files.update(self.files)
561 self._set_control_file_parsing_expectations(True, all_files,
562 new_files.iteritems())
563
564
565 def _set_control_file_parsing_expectations(self, already_stubbed,
566 file_list, files_to_parse):
567 """Expect an attempt to parse the 'control files' in |files|.
568
569 @param already_stubbed: parse_control_string already stubbed out.
570 @param file_list: the files the dev server returns
571 @param files_to_parse: the {'name': FakeControlData} dict of files we
572 expect to get parsed.
573 """
574 if not already_stubbed:
575 self.mox.StubOutWithMock(control_data, 'parse_control_string')
576
577 self.getter.get_control_file_list().AndReturn(file_list)
578 for file, data in files_to_parse:
579 self.getter.get_control_file_contents(
580 file).InAnyOrder().AndReturn(data.string)
Chris Masone6fed6462011-10-20 16:36:43 -0700581 control_data.parse_control_string(
Chris Masoneed356392012-05-08 14:07:13 -0700582 data.string, raise_warnings=True).InAnyOrder().AndReturn(data)
Chris Masone6fed6462011-10-20 16:36:43 -0700583
584
585 def testFindAndParseStableTests(self):
586 """Should find only non-experimental tests that match a predicate."""
587 self.expect_control_file_parsing()
588 self.mox.ReplayAll()
589
590 predicate = lambda d: d.text == self.files['two'].string
591 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter, predicate)
592 self.assertEquals(len(tests), 1)
593 self.assertEquals(tests[0], self.files['two'])
594
595
596 def testFindAndParseTests(self):
597 """Should find all tests that match a predicate."""
598 self.expect_control_file_parsing()
599 self.mox.ReplayAll()
600
601 predicate = lambda d: d.text != self.files['two'].string
602 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter,
603 predicate,
604 add_experimental=True)
605 self.assertEquals(len(tests), 2)
606 self.assertTrue(self.files['one'] in tests)
607 self.assertTrue(self.files['three'] in tests)
608
609
610 def mock_control_file_parsing(self):
611 """Fake out find_and_parse_tests(), returning content from |self.files|.
612 """
613 for test in self.files.values():
614 test.text = test.string # mimic parsing.
615 self.mox.StubOutWithMock(dynamic_suite.Suite, 'find_and_parse_tests')
616 dynamic_suite.Suite.find_and_parse_tests(
617 mox.IgnoreArg(),
618 mox.IgnoreArg(),
619 add_experimental=True).AndReturn(self.files.values())
620
621
622 def testStableUnstableFilter(self):
623 """Should distinguish between experimental and stable tests."""
624 self.mock_control_file_parsing()
625 self.mox.ReplayAll()
626 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
Chris Masoned6f38c82012-02-22 14:53:42 -0800627 afe=self.afe, tko=self.tko)
Chris Masone6fed6462011-10-20 16:36:43 -0700628
629 self.assertTrue(self.files['one'] in suite.tests)
630 self.assertTrue(self.files['two'] in suite.tests)
631 self.assertTrue(self.files['one'] in suite.unstable_tests())
632 self.assertTrue(self.files['two'] in suite.stable_tests())
633 self.assertFalse(self.files['one'] in suite.stable_tests())
634 self.assertFalse(self.files['two'] in suite.unstable_tests())
635
636
637 def expect_job_scheduling(self, add_experimental):
638 """Expect jobs to be scheduled for 'tests' in |self.files|.
639
640 @param add_experimental: expect jobs for experimental tests as well.
641 """
642 for test in self.files.values():
643 if not add_experimental and test.experimental:
644 continue
645 self.afe.create_job(
646 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800647 name=mox.And(mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700648 mox.StrContains(test.name)),
649 control_type=mox.IgnoreArg(),
Chris Masone8b7cd422012-02-22 13:16:11 -0800650 meta_hosts=[dynamic_suite.VERSION_PREFIX + self._BUILD],
Chris Masone52c7fb72012-05-07 14:12:05 -0700651 dependencies=[]).AndReturn(FakeJob())
Chris Masone6fed6462011-10-20 16:36:43 -0700652
653
654 def testScheduleTests(self):
655 """Should schedule stable and experimental tests with the AFE."""
656 self.mock_control_file_parsing()
657 self.expect_job_scheduling(add_experimental=True)
658
659 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800660 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800661 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800662 suite.schedule()
Chris Masone6fed6462011-10-20 16:36:43 -0700663
664
Scott Zawalski9ece6532012-02-28 14:10:47 -0500665 def testScheduleTestsAndRecord(self):
666 """Should schedule stable and experimental tests with the AFE."""
667 self.mock_control_file_parsing()
668 self.mox.ReplayAll()
669 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
670 afe=self.afe, tko=self.tko,
671 results_dir=self.tmpdir)
672 self.mox.ResetAll()
673 self.expect_job_scheduling(add_experimental=True)
674 self.mox.StubOutWithMock(suite, '_record_scheduled_jobs')
675 suite._record_scheduled_jobs()
676 self.mox.ReplayAll()
677 suite.schedule()
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500678 for job in suite._jobs:
679 self.assertTrue(hasattr(job, 'test_name'))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500680
681
Chris Masone6fed6462011-10-20 16:36:43 -0700682 def testScheduleStableTests(self):
683 """Should schedule only stable tests with the AFE."""
684 self.mock_control_file_parsing()
685 self.expect_job_scheduling(add_experimental=False)
686
687 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800688 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800689 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800690 suite.schedule(add_experimental=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700691
692
693 def expect_result_gathering(self, job):
694 self.afe.get_jobs(id=job.id, finished=True).AndReturn(job)
695 entries = map(lambda s: s.entry, job.statuses)
696 self.afe.run('get_host_queue_entries',
697 job=job.id).AndReturn(entries)
698 if True not in map(lambda e: 'aborted' in e and e['aborted'], entries):
699 self.tko.get_status_counts(job=job.id).AndReturn(job.statuses)
700
701
Chris Masonefef21382012-01-17 11:16:32 -0800702 def _createSuiteWithMockedTestsAndControlFiles(self):
703 """Create a Suite, using mocked tests and control file contents.
704
705 @return Suite object, after mocking out behavior needed to create it.
706 """
Chris Masonefef21382012-01-17 11:16:32 -0800707 self.expect_control_file_parsing()
708 self.mox.ReplayAll()
Scott Zawalski9ece6532012-02-28 14:10:47 -0500709 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800710 self.getter, self.afe,
711 self.tko)
Chris Masonefef21382012-01-17 11:16:32 -0800712 self.mox.ResetAll()
713 return suite
714
715
Chris Masone6fed6462011-10-20 16:36:43 -0700716 def testWaitForResults(self):
717 """Should gather status and return records for job summaries."""
718 class FakeStatus(object):
719 """Fake replacement for server-side job status objects.
720
721 @var status: 'GOOD', 'FAIL', 'ERROR', etc.
722 @var test_name: name of the test this is status for
723 @var reason: reason for failure, if any
724 @var aborted: present and True if the job was aborted. Optional.
725 """
726 def __init__(self, code, name, reason, aborted=None):
727 self.status = code
728 self.test_name = name
729 self.reason = reason
730 self.entry = {}
Chris Masone99378582012-04-30 13:10:58 -0700731 self.test_started_time = '2012-11-11 11:11:11'
732 self.test_finished_time = '2012-11-11 12:12:12'
Chris Masone6fed6462011-10-20 16:36:43 -0700733 if aborted:
734 self.entry['aborted'] = True
735
Chris Masone99378582012-04-30 13:10:58 -0700736 def equals_record(self, status):
Chris Masone6fed6462011-10-20 16:36:43 -0700737 """Compares this object to a recorded status."""
Chris Masone99378582012-04-30 13:10:58 -0700738 return self._equals_record(status._status, status._test_name,
739 status._reason)
Chris Masone6fed6462011-10-20 16:36:43 -0700740
Chris Masone99378582012-04-30 13:10:58 -0700741 def _equals_record(self, status, name, reason=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700742 """Compares this object and fields of recorded status."""
743 if 'aborted' in self.entry and self.entry['aborted']:
744 return status == 'ABORT'
745 return (self.status == status and
746 self.test_name == name and
747 self.reason == reason)
748
Chris Masonefef21382012-01-17 11:16:32 -0800749 suite = self._createSuiteWithMockedTestsAndControlFiles()
Chris Masone6fed6462011-10-20 16:36:43 -0700750
751 jobs = [FakeJob(0, [FakeStatus('GOOD', 'T0', ''),
752 FakeStatus('GOOD', 'T1', '')]),
753 FakeJob(1, [FakeStatus('ERROR', 'T0', 'err', False),
754 FakeStatus('GOOD', 'T1', '')]),
755 FakeJob(2, [FakeStatus('TEST_NA', 'T0', 'no')]),
756 FakeJob(2, [FakeStatus('FAIL', 'T0', 'broken')]),
757 FakeJob(3, [FakeStatus('ERROR', 'T0', 'gah', True)])]
758 # To simulate a job that isn't ready the first time we check.
759 self.afe.get_jobs(id=jobs[0].id, finished=True).AndReturn([])
760 # Expect all the rest of the jobs to be good to go the first time.
761 for job in jobs[1:]:
762 self.expect_result_gathering(job)
763 # Then, expect job[0] to be ready.
764 self.expect_result_gathering(jobs[0])
765 # Expect us to poll twice.
766 self.mox.StubOutWithMock(time, 'sleep')
767 time.sleep(5)
768 time.sleep(5)
769 self.mox.ReplayAll()
770
Chris Masone6fed6462011-10-20 16:36:43 -0700771 suite._jobs = list(jobs)
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500772 results = [result for result in suite.wait_for_results()]
Chris Masone6fed6462011-10-20 16:36:43 -0700773 for job in jobs:
774 for status in job.statuses:
775 self.assertTrue(True in map(status.equals_record, results))
776
777
Chris Masoneed356392012-05-08 14:07:13 -0700778 def schedule_and_expect_these_results(self, suite, results, recorder):
779 self.mox.StubOutWithMock(suite, 'schedule')
780 suite.schedule(True)
Chris Masone6fed6462011-10-20 16:36:43 -0700781 for result in results:
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500782 status = result[0]
Chris Masone99378582012-04-30 13:10:58 -0700783 test_name = result[1]
784 recorder.record_entry(
785 StatusContains.CreateFromStrings('START', test_name))
786 recorder.record_entry(
787 StatusContains.CreateFromStrings(*result)).InAnyOrder('results')
788 recorder.record_entry(
789 StatusContains.CreateFromStrings('END %s' % status, test_name))
Chris Masone6fed6462011-10-20 16:36:43 -0700790 self.mox.StubOutWithMock(suite, 'wait_for_results')
Chris Masone99378582012-04-30 13:10:58 -0700791 suite.wait_for_results().AndReturn(
792 map(lambda r: dynamic_suite.Status(*r), results))
Chris Masoneed356392012-05-08 14:07:13 -0700793
794
795 def testRunAndWaitSuccess(self):
796 """Should record successful results."""
797 suite = self._createSuiteWithMockedTestsAndControlFiles()
798
799 recorder = self.mox.CreateMock(base_job.base_job)
800 recorder.record_entry(
801 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
802
803 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
804 self.schedule_and_expect_these_results(suite, results, recorder)
805 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700806 self.mox.ReplayAll()
807
Chris Masone99378582012-04-30 13:10:58 -0700808 suite.run_and_wait(recorder.record_entry, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700809
810
811 def testRunAndWaitFailure(self):
812 """Should record failure to gather results."""
Chris Masonefef21382012-01-17 11:16:32 -0800813 suite = self._createSuiteWithMockedTestsAndControlFiles()
814
Chris Masone6fed6462011-10-20 16:36:43 -0700815 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700816 recorder.record_entry(
817 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
818 recorder.record_entry(
819 StatusContains.CreateFromStrings('FAIL', self._TAG, 'waiting'))
Chris Masone6fed6462011-10-20 16:36:43 -0700820
Chris Masone6fed6462011-10-20 16:36:43 -0700821 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone8b7cd422012-02-22 13:16:11 -0800822 suite.schedule(True)
Chris Masone6fed6462011-10-20 16:36:43 -0700823 self.mox.StubOutWithMock(suite, 'wait_for_results')
Chris Masone99378582012-04-30 13:10:58 -0700824 suite.wait_for_results().AndRaise(Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700825 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700826 self.mox.ReplayAll()
827
Chris Masone99378582012-04-30 13:10:58 -0700828 suite.run_and_wait(recorder.record_entry, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700829
830
831 def testRunAndWaitScheduleFailure(self):
Chris Masonefef21382012-01-17 11:16:32 -0800832 """Should record failure to schedule jobs."""
833 suite = self._createSuiteWithMockedTestsAndControlFiles()
834
Chris Masone6fed6462011-10-20 16:36:43 -0700835 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700836 recorder.record_entry(
837 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
838 recorder.record_entry(
839 StatusContains.CreateFromStrings('FAIL', self._TAG, 'scheduling'))
Chris Masone6fed6462011-10-20 16:36:43 -0700840
Chris Masone6fed6462011-10-20 16:36:43 -0700841 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone99378582012-04-30 13:10:58 -0700842 suite.schedule(True).AndRaise(Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700843 self.expect_control_file_reparsing()
844 self.mox.ReplayAll()
845
846 suite.run_and_wait(recorder.record_entry, True)
847
848
849 def testRunAndWaitDevServerRacyFailure(self):
850 """Should record discovery of dev server races in listing files."""
851 suite = self._createSuiteWithMockedTestsAndControlFiles()
852
853 recorder = self.mox.CreateMock(base_job.base_job)
854 recorder.record_entry(
855 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
856
857 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
858 self.schedule_and_expect_these_results(suite, results, recorder)
859
860 self.expect_racy_control_file_reparsing(
861 {'new': SuiteTest.FakeControlData('!')})
862
863 recorder.record_entry(
864 StatusContains.CreateFromStrings('FAIL', self._TAG, 'Dev Server'))
Chris Masone6fed6462011-10-20 16:36:43 -0700865 self.mox.ReplayAll()
866
Chris Masone99378582012-04-30 13:10:58 -0700867 suite.run_and_wait(recorder.record_entry, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700868
869
870if __name__ == '__main__':
871 unittest.main()