blob: 1de8e085147e8ed989d982f21f69ce726a3089fe [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
Alex Miller7a2160a2012-08-01 13:18:59 -070011import os
Chris Masoned368cc42012-03-07 15:16:59 -080012import random
Chris Masone6fed6462011-10-20 16:36:43 -070013import shutil
Alex Miller7a2160a2012-08-01 13:18:59 -070014import signal
Chris Masone6fed6462011-10-20 16:36:43 -070015import tempfile
16import time
17import unittest
18
Chris Masonef8b53062012-05-08 22:14:18 -070019from autotest_lib.client.common_lib import base_job, control_data, error
20from autotest_lib.client.common_lib import global_config
Chris Sosa24b3a022012-07-31 14:27:59 -070021from autotest_lib.client.common_lib.cros import dev_server
Chris Masone47c9e642012-04-25 14:22:18 -070022from autotest_lib.frontend.afe.json_rpc import proxy
Chris Masone6fed6462011-10-20 16:36:43 -070023from autotest_lib.server.cros import control_file_getter, dynamic_suite
Chris Masone275ec902012-07-10 15:28:34 -070024from autotest_lib.server.cros import host_lock_manager, job_status
Chris Masone8d6e6412012-06-28 11:20:56 -070025from autotest_lib.server.cros.dynamic_suite_fakes import FakeControlData
26from autotest_lib.server.cros.dynamic_suite_fakes import FakeHost, FakeJob
Chris Masone6ea0cad2012-07-02 09:43:36 -070027from autotest_lib.server.cros.dynamic_suite_fakes import FakeLabel
Chris Masone6fed6462011-10-20 16:36:43 -070028from autotest_lib.server import frontend
29
Chris Masone5374c672012-03-05 15:11:39 -080030
Chris Masonef63576d2012-06-29 11:13:31 -070031class StatusContains(mox.Comparator):
32 @staticmethod
33 def CreateFromStrings(status=None, test_name=None, reason=None):
34 status_comp = mox.StrContains(status) if status else mox.IgnoreArg()
35 name_comp = mox.StrContains(test_name) if test_name else mox.IgnoreArg()
36 reason_comp = mox.StrContains(reason) if reason else mox.IgnoreArg()
37 return StatusContains(status_comp, name_comp, reason_comp)
38
39
40 def __init__(self, status=mox.IgnoreArg(), test_name=mox.IgnoreArg(),
41 reason=mox.IgnoreArg()):
42 """Initialize.
43
44 Takes mox.Comparator objects to apply to dynamic_suite.Status
45 member variables.
46
47 @param status: status code, e.g. 'INFO', 'START', etc.
48 @param test_name: expected test name.
49 @param reason: expected reason
50 """
51 self._status = status
52 self._test_name = test_name
53 self._reason = reason
54
55
56 def equals(self, rhs):
57 """Check to see if fields match base_job.status_log_entry obj in rhs.
58
59 @param rhs: base_job.status_log_entry object to match.
60 @return boolean
61 """
62 return (self._status.equals(rhs.status_code) and
63 self._test_name.equals(rhs.operation) and
64 self._reason.equals(rhs.message))
65
66
67 def __repr__(self):
68 return '<Status containing \'%s\t%s\t%s\'>' % (self._status,
69 self._test_name,
70 self._reason)
71
72
Chris Masoneab3e7332012-02-29 18:54:58 -080073class DynamicSuiteTest(mox.MoxTestBase):
74 """Unit tests for dynamic_suite module methods.
75
76 @var _DARGS: default args to vet.
77 """
78
79
80 def setUp(self):
81 super(DynamicSuiteTest, self).setUp()
82 self._DARGS = {'name': 'name',
83 'build': 'build',
84 'board': 'board',
85 'job': self.mox.CreateMock(base_job.base_job),
86 'num': 1,
87 'pool': 'pool',
88 'skip_reimage': True,
Chris Masone62579122012-03-08 15:18:43 -080089 'check_hosts': False,
Chris Masoneab3e7332012-02-29 18:54:58 -080090 'add_experimental': False}
91
92
93 def testVetRequiredReimageAndRunArgs(self):
94 """Should verify only that required args are present and correct."""
Chris Masone6dff2432012-08-08 13:29:32 -070095 spec = dynamic_suite.SuiteSpec(**self._DARGS)
96 self.assertEquals(spec.build, self._DARGS['build'])
97 self.assertEquals(spec.board, 'board:' + self._DARGS['board'])
98 self.assertEquals(spec.name, self._DARGS['name'])
99 self.assertEquals(spec.job, self._DARGS['job'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800100
101
102 def testVetReimageAndRunBuildArgFail(self):
103 """Should fail verification because |build| arg is bad."""
104 self._DARGS['build'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700105 self.assertRaises(error.SuiteArgumentException,
Chris Masone6dff2432012-08-08 13:29:32 -0700106 dynamic_suite.SuiteSpec,
Chris Masoneab3e7332012-02-29 18:54:58 -0800107 **self._DARGS)
108
109
110 def testVetReimageAndRunBoardArgFail(self):
111 """Should fail verification because |board| arg is bad."""
112 self._DARGS['board'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700113 self.assertRaises(error.SuiteArgumentException,
Chris Masone6dff2432012-08-08 13:29:32 -0700114 dynamic_suite.SuiteSpec,
Chris Masoneab3e7332012-02-29 18:54:58 -0800115 **self._DARGS)
116
117
118 def testVetReimageAndRunNameArgFail(self):
119 """Should fail verification because |name| arg is bad."""
120 self._DARGS['name'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700121 self.assertRaises(error.SuiteArgumentException,
Chris Masone6dff2432012-08-08 13:29:32 -0700122 dynamic_suite.SuiteSpec,
Chris Masoneab3e7332012-02-29 18:54:58 -0800123 **self._DARGS)
124
125
126 def testVetReimageAndRunJobArgFail(self):
127 """Should fail verification because |job| arg is bad."""
128 self._DARGS['job'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700129 self.assertRaises(error.SuiteArgumentException,
Chris Masone6dff2432012-08-08 13:29:32 -0700130 dynamic_suite.SuiteSpec,
Chris Masoneab3e7332012-02-29 18:54:58 -0800131 **self._DARGS)
132
133
134 def testOverrideOptionalReimageAndRunArgs(self):
135 """Should verify that optional args can be overridden."""
Chris Masone6dff2432012-08-08 13:29:32 -0700136 spec = dynamic_suite.SuiteSpec(**self._DARGS)
137 self.assertEquals(spec.pool, 'pool:' + self._DARGS['pool'])
138 self.assertEquals(spec.num, self._DARGS['num'])
139 self.assertEquals(spec.check_hosts, self._DARGS['check_hosts'])
140 self.assertEquals(spec.skip_reimage, self._DARGS['skip_reimage'])
141 self.assertEquals(spec.add_experimental,
142 self._DARGS['add_experimental'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800143
144
145 def testDefaultOptionalReimageAndRunArgs(self):
146 """Should verify that optional args get defaults."""
147 del(self._DARGS['pool'])
148 del(self._DARGS['skip_reimage'])
Chris Masone62579122012-03-08 15:18:43 -0800149 del(self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800150 del(self._DARGS['add_experimental'])
151 del(self._DARGS['num'])
Chris Masone6dff2432012-08-08 13:29:32 -0700152
153 spec = dynamic_suite.SuiteSpec(**self._DARGS)
154 self.assertEquals(spec.pool, None)
155 self.assertEquals(spec.num, None)
156 self.assertEquals(spec.check_hosts, True)
157 self.assertEquals(spec.skip_reimage, False)
158 self.assertEquals(spec.add_experimental, True)
Chris Masoneab3e7332012-02-29 18:54:58 -0800159
160
Alex Miller7a2160a2012-08-01 13:18:59 -0700161 def testReimageAndSIGTERM(self):
162 """Should reimage_and_run that causes a SIGTERM and fails cleanly."""
163 def suicide():
164 os.kill(os.getpid(), signal.SIGTERM)
165
166 # mox does not play nicely with receiving a bare SIGTERM, but it does
167 # play nicely with unhandled exceptions...
168 class UnhandledSIGTERM(Exception):
169 pass
170
171 self.mox.StubOutWithMock(dev_server.DevServer, 'create')
172 dev_server.DevServer.create().WithSideEffects(suicide)
173 manager = self.mox.CreateMock(host_lock_manager.HostLockManager)
174 manager.unlock()
175 spec = self.mox.CreateMock(dynamic_suite.SuiteSpec)
176 spec.skip_reimage = True
177
178 self.mox.ReplayAll()
179
180 with dynamic_suite.SignalsAsExceptions(UnhandledSIGTERM):
181 self.assertRaises(error.SignalException,
182 dynamic_suite._perform_reimage_and_run,
183 spec, None, None, None, manager)
184
185
Chris Masone6fed6462011-10-20 16:36:43 -0700186class ReimagerTest(mox.MoxTestBase):
187 """Unit tests for dynamic_suite.Reimager.
188
189 @var _URL: fake image url
Chris Masone8b7cd422012-02-22 13:16:11 -0800190 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700191 @var _NUM: fake number of machines to run on
192 @var _BOARD: fake board to reimage
193 """
194
Chris Sosa24b3a022012-07-31 14:27:59 -0700195 _DEVSERVER_URL = 'http://nothing:8082'
196 _URL = '%s/%s'
Chris Masone8b7cd422012-02-22 13:16:11 -0800197 _BUILD = 'build'
Chris Masone6fed6462011-10-20 16:36:43 -0700198 _NUM = 4
199 _BOARD = 'board'
Chris Masone5552dd72012-02-15 15:01:04 -0800200 _CONFIG = global_config.global_config
Chris Masone6fed6462011-10-20 16:36:43 -0700201
202
203 def setUp(self):
204 super(ReimagerTest, self).setUp()
205 self.afe = self.mox.CreateMock(frontend.AFE)
206 self.tko = self.mox.CreateMock(frontend.TKO)
Chris Masone275ec902012-07-10 15:28:34 -0700207 self.manager = self.mox.CreateMock(host_lock_manager.HostLockManager)
Chris Masone6fed6462011-10-20 16:36:43 -0700208 self.reimager = dynamic_suite.Reimager('', afe=self.afe, tko=self.tko)
Chris Masone5552dd72012-02-15 15:01:04 -0800209 self._CONFIG.override_config_value('CROS',
210 'sharding_factor',
211 "%d" % self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700212
213
214 def testEnsureVersionLabelAlreadyExists(self):
Chris Masone47c9e642012-04-25 14:22:18 -0700215 """Should tolerate a label that already exists."""
Chris Masone6fed6462011-10-20 16:36:43 -0700216 name = 'label'
Chris Masone47c9e642012-04-25 14:22:18 -0700217 error = proxy.ValidationError(
218 {'name': 'ValidationError',
219 'message': '{"name": "This value must be unique"}',
220 'traceback': ''},
221 'BAD')
222 self.afe.create_label(name=name).AndRaise(error)
Chris Masone6fed6462011-10-20 16:36:43 -0700223 self.mox.ReplayAll()
224 self.reimager._ensure_version_label(name)
225
226
227 def testEnsureVersionLabel(self):
228 """Should create a label if it doesn't already exist."""
229 name = 'label'
Chris Masone6fed6462011-10-20 16:36:43 -0700230 self.afe.create_label(name=name)
231 self.mox.ReplayAll()
232 self.reimager._ensure_version_label(name)
233
234
Chris Masone5374c672012-03-05 15:11:39 -0800235 def testCountHostsByBoardAndPool(self):
236 """Should count available hosts by board and pool."""
237 spec = [self._BOARD, 'pool:bvt']
238 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()])
239 self.mox.ReplayAll()
240 self.assertEquals(self.reimager._count_usable_hosts(spec), 1)
241
242
243 def testCountHostsByBoard(self):
244 """Should count available hosts by board."""
245 spec = [self._BOARD]
246 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()] * 2)
247 self.mox.ReplayAll()
248 self.assertEquals(self.reimager._count_usable_hosts(spec), 2)
249
250
251 def testCountZeroHostsByBoard(self):
252 """Should count the available hosts, by board, getting zero."""
253 spec = [self._BOARD]
254 self.afe.get_hosts(multiple_labels=spec).AndReturn([])
255 self.mox.ReplayAll()
256 self.assertEquals(self.reimager._count_usable_hosts(spec), 0)
257
258
Chris Masone6fed6462011-10-20 16:36:43 -0700259 def testInjectVars(self):
260 """Should inject dict of varibles into provided strings."""
261 def find_all_in(d, s):
262 """Returns true if all key-value pairs in |d| are printed in |s|."""
Chris Sosa24b3a022012-07-31 14:27:59 -0700263 for k, v in d.iteritems():
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800264 if isinstance(v, str):
Chris Sosa24b3a022012-07-31 14:27:59 -0700265 if "%s='%s'\n" % (k, v) not in s:
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800266 return False
267 else:
Chris Sosa24b3a022012-07-31 14:27:59 -0700268 if "%s=%r\n" % (k, v) not in s:
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800269 return False
270 return True
Chris Masone6fed6462011-10-20 16:36:43 -0700271
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800272 v = {'v1': 'one', 'v2': 'two', 'v3': None, 'v4': False, 'v5': 5}
Chris Masone8b764252012-01-17 11:12:51 -0800273 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, '')))
274 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, 'ctrl')))
Chris Masone6fed6462011-10-20 16:36:43 -0700275
276
Chris Masone6fed6462011-10-20 16:36:43 -0700277 def testScheduleJob(self):
278 """Should be able to create a job with the AFE."""
279 # Fake out getting the autoupdate control file contents.
280 cf_getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
281 cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
282 self.reimager._cf_getter = cf_getter
Chris Sosa24b3a022012-07-31 14:27:59 -0700283 self._CONFIG.override_config_value('CROS',
284 'dev_server',
285 self._DEVSERVER_URL)
Chris Masone6dca10a2012-02-15 15:41:42 -0800286 self._CONFIG.override_config_value('CROS',
287 'image_url_pattern',
288 self._URL)
Chris Masone6fed6462011-10-20 16:36:43 -0700289 self.afe.create_job(
Chris Sosa24b3a022012-07-31 14:27:59 -0700290 control_file=mox.And(
291 mox.StrContains(self._BUILD),
292 mox.StrContains(self._URL % (self._DEVSERVER_URL,
293 self._BUILD))),
Chris Masone8b7cd422012-02-22 13:16:11 -0800294 name=mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700295 control_type='Server',
Chris Masone5374c672012-03-05 15:11:39 -0800296 meta_hosts=[self._BOARD] * self._NUM,
Chris Masone99378582012-04-30 13:10:58 -0700297 dependencies=[],
298 priority='Low')
Chris Masone6fed6462011-10-20 16:36:43 -0700299 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700300 self.reimager._schedule_reimage_job(self._BUILD, self._BOARD, None,
301 self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700302
Chris Sosa24b3a022012-07-31 14:27:59 -0700303 def testPackageUrl(self):
304 """Should be able to get the package_url for any build."""
305 self._CONFIG.override_config_value('CROS',
306 'dev_server',
307 self._DEVSERVER_URL)
308 self._CONFIG.override_config_value('CROS',
309 'package_url_pattern',
310 self._URL)
311 self.mox.ReplayAll()
312 package_url = dynamic_suite.get_package_url(self._BUILD)
313 self.assertEqual(package_url, self._URL % (self._DEVSERVER_URL,
314 self._BUILD))
Chris Masone6fed6462011-10-20 16:36:43 -0700315
Chris Masone275ec902012-07-10 15:28:34 -0700316 def expect_attempt(self, canary_job, statuses, ex=None, check_hosts=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700317 """Sets up |self.reimager| to expect an attempt() that returns |success|
318
Chris Masone6ea0cad2012-07-02 09:43:36 -0700319 Also stubs out Reimager._clear_build_state(), should the caller wish
Chris Masoned368cc42012-03-07 15:16:59 -0800320 to set an expectation there as well.
321
Chris Masone275ec902012-07-10 15:28:34 -0700322 @param canary_job: a FakeJob representing the job we're expecting.
323 @param statuses: dict mapping a hostname to its job_status.Status.
324 Will be returned by job_status.gather_per_host_results
Chris Masone796fcf12012-02-22 16:53:31 -0800325 @param ex: if not None, |ex| is raised by get_jobs()
Chris Masone6fed6462011-10-20 16:36:43 -0700326 @return a FakeJob configured with appropriate expectations
327 """
Chris Masone6fed6462011-10-20 16:36:43 -0700328 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
Chris Masone6fed6462011-10-20 16:36:43 -0700329 self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
Chris Masone275ec902012-07-10 15:28:34 -0700330 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
331 self.mox.StubOutWithMock(self.reimager, '_clear_build_state')
332
Chris Masone517ef482012-07-23 15:36:36 -0700333 self.mox.StubOutWithMock(job_status, 'wait_for_jobs_to_start')
Chris Masone275ec902012-07-10 15:28:34 -0700334 self.mox.StubOutWithMock(job_status, 'wait_for_and_lock_job_hosts')
335 self.mox.StubOutWithMock(job_status, 'gather_job_hostnames')
Chris Masone517ef482012-07-23 15:36:36 -0700336 self.mox.StubOutWithMock(job_status, 'wait_for_jobs_to_finish')
Chris Masone275ec902012-07-10 15:28:34 -0700337 self.mox.StubOutWithMock(job_status, 'gather_per_host_results')
338 self.mox.StubOutWithMock(job_status, 'record_and_report_results')
339
Chris Masone275ec902012-07-10 15:28:34 -0700340 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone8b7cd422012-02-22 13:16:11 -0800341 self.reimager._schedule_reimage_job(self._BUILD,
Chris Masonec43448f2012-05-31 12:55:59 -0700342 self._BOARD,
343 None,
Chris Masone275ec902012-07-10 15:28:34 -0700344 self._NUM).AndReturn(canary_job)
Chris Masone62579122012-03-08 15:18:43 -0800345 if check_hosts:
Chris Masone62579122012-03-08 15:18:43 -0800346 self.reimager._count_usable_hosts(
347 mox.IgnoreArg()).AndReturn(self._NUM)
Chris Masone5374c672012-03-05 15:11:39 -0800348
Chris Masone517ef482012-07-23 15:36:36 -0700349 job_status.wait_for_jobs_to_start(self.afe, [canary_job])
Chris Masone275ec902012-07-10 15:28:34 -0700350 job_status.wait_for_and_lock_job_hosts(
Chris Masone517ef482012-07-23 15:36:36 -0700351 self.afe, [canary_job], self.manager).AndReturn(statuses.keys())
Chris Masone275ec902012-07-10 15:28:34 -0700352
353 if ex:
Chris Masone517ef482012-07-23 15:36:36 -0700354 job_status.wait_for_jobs_to_finish(self.afe,
355 [canary_job]).AndRaise(ex)
Chris Masone796fcf12012-02-22 16:53:31 -0800356 else:
Chris Masone517ef482012-07-23 15:36:36 -0700357 job_status.wait_for_jobs_to_finish(self.afe, [canary_job])
Chris Masone6ea0cad2012-07-02 09:43:36 -0700358 job_status.gather_per_host_results(
Chris Masone275ec902012-07-10 15:28:34 -0700359 mox.IgnoreArg(), mox.IgnoreArg(), [canary_job],
Chris Masone6ea0cad2012-07-02 09:43:36 -0700360 mox.StrContains(dynamic_suite.REIMAGE_JOB_NAME)).AndReturn(
361 statuses)
362
363 if statuses:
Chris Sosa24b3a022012-07-31 14:27:59 -0700364 ret_val = reduce(lambda v, s: v and s.is_good(),
Chris Masone6ea0cad2012-07-02 09:43:36 -0700365 statuses.values(), True)
Chris Masone6ea0cad2012-07-02 09:43:36 -0700366 job_status.record_and_report_results(
367 statuses.values(), mox.IgnoreArg()).AndReturn(ret_val)
Chris Masone6fed6462011-10-20 16:36:43 -0700368
Chris Masone6fed6462011-10-20 16:36:43 -0700369
370 def testSuccessfulReimage(self):
371 """Should attempt a reimage and record success."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700372 canary = FakeJob()
Chris Masone517ef482012-07-23 15:36:36 -0700373 statuses = {canary.hostnames[0]: job_status.Status('GOOD',
374 canary.hostnames[0])}
Chris Masone6ea0cad2012-07-02 09:43:36 -0700375 self.expect_attempt(canary, statuses)
Chris Masone6fed6462011-10-20 16:36:43 -0700376
377 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone517ef482012-07-23 15:36:36 -0700378 self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
Chris Masone6fed6462011-10-20 16:36:43 -0700379 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700380 self.assertTrue(self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700381 rjob.record_entry, True,
382 self.manager))
Chris Masoned368cc42012-03-07 15:16:59 -0800383 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700384
385
386 def testFailedReimage(self):
387 """Should attempt a reimage and record failure."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700388 canary = FakeJob()
Chris Masone517ef482012-07-23 15:36:36 -0700389 statuses = {canary.hostnames[0]: job_status.Status('FAIL',
390 canary.hostnames[0])}
Chris Masone6ea0cad2012-07-02 09:43:36 -0700391 self.expect_attempt(canary, statuses)
Chris Masone6fed6462011-10-20 16:36:43 -0700392
393 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone517ef482012-07-23 15:36:36 -0700394 self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
Chris Masone6fed6462011-10-20 16:36:43 -0700395 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700396 self.assertFalse(self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700397 rjob.record_entry, True,
398 self.manager))
Chris Masoned368cc42012-03-07 15:16:59 -0800399 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700400
401
402 def testReimageThatNeverHappened(self):
403 """Should attempt a reimage and record that it didn't run."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700404 canary = FakeJob()
405 statuses = {'hostless': job_status.Status('ABORT', 'big_job_name')}
406 self.expect_attempt(canary, statuses)
Chris Masone6fed6462011-10-20 16:36:43 -0700407
408 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone6fed6462011-10-20 16:36:43 -0700409 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700410 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700411 rjob.record_entry, True, self.manager)
Chris Masoned368cc42012-03-07 15:16:59 -0800412 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700413
414
Chris Masone796fcf12012-02-22 16:53:31 -0800415 def testReimageThatRaised(self):
416 """Should attempt a reimage that raises an exception and record that."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700417 canary = FakeJob()
Chris Masone796fcf12012-02-22 16:53:31 -0800418 ex_message = 'Oh no!'
Chris Masone6ea0cad2012-07-02 09:43:36 -0700419 self.expect_attempt(canary, statuses={}, ex=Exception(ex_message))
Chris Masone796fcf12012-02-22 16:53:31 -0800420
421 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700422 rjob.record_entry(StatusContains.CreateFromStrings('START'))
423 rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
424 reason=ex_message))
425 rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
Chris Masone796fcf12012-02-22 16:53:31 -0800426 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700427 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700428 rjob.record_entry, True, self.manager)
Chris Masone62579122012-03-08 15:18:43 -0800429 self.reimager.clear_reimaged_host_state(self._BUILD)
430
431
432 def testSuccessfulReimageThatCouldNotScheduleRightAway(self):
Chris Masone99378582012-04-30 13:10:58 -0700433 """Should attempt reimage, ignoring host availability; record success.
Chris Masone62579122012-03-08 15:18:43 -0800434 """
Chris Masone6ea0cad2012-07-02 09:43:36 -0700435 canary = FakeJob()
Chris Masone517ef482012-07-23 15:36:36 -0700436 statuses = {canary.hostnames[0]: job_status.Status('GOOD',
437 canary.hostnames[0])}
Chris Masone6ea0cad2012-07-02 09:43:36 -0700438 self.expect_attempt(canary, statuses, check_hosts=False)
Chris Masone62579122012-03-08 15:18:43 -0800439
440 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone517ef482012-07-23 15:36:36 -0700441 self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
Chris Masone62579122012-03-08 15:18:43 -0800442 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700443 self.assertTrue(self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700444 rjob.record_entry, False,
445 self.manager))
Chris Masoned368cc42012-03-07 15:16:59 -0800446 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone796fcf12012-02-22 16:53:31 -0800447
448
Chris Masone5374c672012-03-05 15:11:39 -0800449 def testReimageThatCouldNotSchedule(self):
450 """Should attempt a reimage that can't be scheduled."""
Chris Masone502b71e2012-04-10 10:41:35 -0700451 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
452 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
453
454 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
455 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(1)
456
457 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700458 rjob.record_entry(StatusContains.CreateFromStrings('START'))
459 rjob.record_entry(
460 StatusContains.CreateFromStrings('WARN', reason='Too few hosts'))
461 rjob.record_entry(StatusContains.CreateFromStrings('END WARN'))
Chris Masone502b71e2012-04-10 10:41:35 -0700462 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700463 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700464 rjob.record_entry, True, self.manager)
Chris Masone502b71e2012-04-10 10:41:35 -0700465 self.reimager.clear_reimaged_host_state(self._BUILD)
466
467
468 def testReimageWithNoAvailableHosts(self):
469 """Should attempt a reimage while all hosts are dead."""
Chris Masone62579122012-03-08 15:18:43 -0800470 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
471 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone5374c672012-03-05 15:11:39 -0800472
473 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
474 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(0)
475
476 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700477 rjob.record_entry(StatusContains.CreateFromStrings('START'))
478 rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
479 reason='All hosts'))
480 rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
Chris Masone5374c672012-03-05 15:11:39 -0800481 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700482 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700483 rjob.record_entry, True, self.manager)
Chris Masoned368cc42012-03-07 15:16:59 -0800484 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone5374c672012-03-05 15:11:39 -0800485
486
Chris Masone6fed6462011-10-20 16:36:43 -0700487class SuiteTest(mox.MoxTestBase):
488 """Unit tests for dynamic_suite.Suite.
489
Chris Masone8b7cd422012-02-22 13:16:11 -0800490 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700491 @var _TAG: fake suite tag
492 """
493
Chris Masone8b7cd422012-02-22 13:16:11 -0800494 _BUILD = 'build'
495 _TAG = 'suite_tag'
Chris Masone6fed6462011-10-20 16:36:43 -0700496
497
498 def setUp(self):
Chris Masone6fed6462011-10-20 16:36:43 -0700499 super(SuiteTest, self).setUp()
500 self.afe = self.mox.CreateMock(frontend.AFE)
501 self.tko = self.mox.CreateMock(frontend.TKO)
502
503 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__)
504
Chris Masone275ec902012-07-10 15:28:34 -0700505 self.manager = self.mox.CreateMock(host_lock_manager.HostLockManager)
Chris Masone6fed6462011-10-20 16:36:43 -0700506 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
507
Chris Masone8d6e6412012-06-28 11:20:56 -0700508 self.files = {'one': FakeControlData(self._TAG, 'data_one', expr=True),
509 'two': FakeControlData(self._TAG, 'data_two'),
510 'three': FakeControlData(self._TAG, 'data_three')}
Chris Masone6fed6462011-10-20 16:36:43 -0700511
Chris Masone75a20612012-05-08 12:37:31 -0700512 self.files_to_filter = {
Chris Masone8d6e6412012-06-28 11:20:56 -0700513 'with/deps/...': FakeControlData(self._TAG, 'gets filtered'),
514 'with/profilers/...': FakeControlData(self._TAG, 'gets filtered')}
Chris Masone75a20612012-05-08 12:37:31 -0700515
Chris Masone6fed6462011-10-20 16:36:43 -0700516
517 def tearDown(self):
518 super(SuiteTest, self).tearDown()
519 shutil.rmtree(self.tmpdir, ignore_errors=True)
520
521
522 def expect_control_file_parsing(self):
523 """Expect an attempt to parse the 'control files' in |self.files|."""
Chris Masone75a20612012-05-08 12:37:31 -0700524 all_files = self.files.keys() + self.files_to_filter.keys()
Chris Masoneed356392012-05-08 14:07:13 -0700525 self._set_control_file_parsing_expectations(False, all_files,
526 self.files.iteritems())
527
528
529 def expect_control_file_reparsing(self):
530 """Expect re-parsing the 'control files' in |self.files|."""
531 all_files = self.files.keys() + self.files_to_filter.keys()
532 self._set_control_file_parsing_expectations(True, all_files,
533 self.files.iteritems())
534
535
536 def expect_racy_control_file_reparsing(self, new_files):
537 """Expect re-fetching and parsing of control files to return extra.
538
539 @param new_files: extra control files that showed up during scheduling.
540 """
541 all_files = (self.files.keys() + self.files_to_filter.keys() +
542 new_files.keys())
543 new_files.update(self.files)
544 self._set_control_file_parsing_expectations(True, all_files,
545 new_files.iteritems())
546
547
548 def _set_control_file_parsing_expectations(self, already_stubbed,
549 file_list, files_to_parse):
550 """Expect an attempt to parse the 'control files' in |files|.
551
552 @param already_stubbed: parse_control_string already stubbed out.
553 @param file_list: the files the dev server returns
554 @param files_to_parse: the {'name': FakeControlData} dict of files we
555 expect to get parsed.
556 """
557 if not already_stubbed:
558 self.mox.StubOutWithMock(control_data, 'parse_control_string')
559
560 self.getter.get_control_file_list().AndReturn(file_list)
561 for file, data in files_to_parse:
562 self.getter.get_control_file_contents(
563 file).InAnyOrder().AndReturn(data.string)
Chris Masone6fed6462011-10-20 16:36:43 -0700564 control_data.parse_control_string(
Chris Masoneed356392012-05-08 14:07:13 -0700565 data.string, raise_warnings=True).InAnyOrder().AndReturn(data)
Chris Masone6fed6462011-10-20 16:36:43 -0700566
567
568 def testFindAndParseStableTests(self):
569 """Should find only non-experimental tests that match a predicate."""
570 self.expect_control_file_parsing()
571 self.mox.ReplayAll()
572
573 predicate = lambda d: d.text == self.files['two'].string
574 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter, predicate)
575 self.assertEquals(len(tests), 1)
576 self.assertEquals(tests[0], self.files['two'])
577
578
579 def testFindAndParseTests(self):
580 """Should find all tests that match a predicate."""
581 self.expect_control_file_parsing()
582 self.mox.ReplayAll()
583
584 predicate = lambda d: d.text != self.files['two'].string
585 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter,
586 predicate,
587 add_experimental=True)
588 self.assertEquals(len(tests), 2)
589 self.assertTrue(self.files['one'] in tests)
590 self.assertTrue(self.files['three'] in tests)
591
592
593 def mock_control_file_parsing(self):
594 """Fake out find_and_parse_tests(), returning content from |self.files|.
595 """
596 for test in self.files.values():
597 test.text = test.string # mimic parsing.
598 self.mox.StubOutWithMock(dynamic_suite.Suite, 'find_and_parse_tests')
599 dynamic_suite.Suite.find_and_parse_tests(
600 mox.IgnoreArg(),
601 mox.IgnoreArg(),
602 add_experimental=True).AndReturn(self.files.values())
603
604
605 def testStableUnstableFilter(self):
606 """Should distinguish between experimental and stable tests."""
607 self.mock_control_file_parsing()
608 self.mox.ReplayAll()
609 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
Chris Masoned6f38c82012-02-22 14:53:42 -0800610 afe=self.afe, tko=self.tko)
Chris Masone6fed6462011-10-20 16:36:43 -0700611
612 self.assertTrue(self.files['one'] in suite.tests)
613 self.assertTrue(self.files['two'] in suite.tests)
614 self.assertTrue(self.files['one'] in suite.unstable_tests())
615 self.assertTrue(self.files['two'] in suite.stable_tests())
616 self.assertFalse(self.files['one'] in suite.stable_tests())
617 self.assertFalse(self.files['two'] in suite.unstable_tests())
618
619
620 def expect_job_scheduling(self, add_experimental):
621 """Expect jobs to be scheduled for 'tests' in |self.files|.
622
623 @param add_experimental: expect jobs for experimental tests as well.
624 """
625 for test in self.files.values():
626 if not add_experimental and test.experimental:
627 continue
628 self.afe.create_job(
629 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800630 name=mox.And(mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700631 mox.StrContains(test.name)),
632 control_type=mox.IgnoreArg(),
Chris Masone8b7cd422012-02-22 13:16:11 -0800633 meta_hosts=[dynamic_suite.VERSION_PREFIX + self._BUILD],
Chris Masonebafbbb02012-05-16 13:41:36 -0700634 dependencies=[],
635 keyvals={'build': self._BUILD, 'suite': self._TAG}
636 ).AndReturn(FakeJob())
Chris Masone6fed6462011-10-20 16:36:43 -0700637
638
639 def testScheduleTests(self):
640 """Should schedule stable and experimental tests with the AFE."""
641 self.mock_control_file_parsing()
642 self.expect_job_scheduling(add_experimental=True)
643
644 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800645 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800646 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800647 suite.schedule()
Chris Masone6fed6462011-10-20 16:36:43 -0700648
649
Scott Zawalski9ece6532012-02-28 14:10:47 -0500650 def testScheduleTestsAndRecord(self):
651 """Should schedule stable and experimental tests with the AFE."""
652 self.mock_control_file_parsing()
653 self.mox.ReplayAll()
654 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
655 afe=self.afe, tko=self.tko,
656 results_dir=self.tmpdir)
657 self.mox.ResetAll()
658 self.expect_job_scheduling(add_experimental=True)
659 self.mox.StubOutWithMock(suite, '_record_scheduled_jobs')
660 suite._record_scheduled_jobs()
661 self.mox.ReplayAll()
662 suite.schedule()
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500663 for job in suite._jobs:
664 self.assertTrue(hasattr(job, 'test_name'))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500665
666
Chris Masone6fed6462011-10-20 16:36:43 -0700667 def testScheduleStableTests(self):
668 """Should schedule only stable tests with the AFE."""
669 self.mock_control_file_parsing()
670 self.expect_job_scheduling(add_experimental=False)
671
672 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800673 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800674 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800675 suite.schedule(add_experimental=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700676
677
Chris Masonefef21382012-01-17 11:16:32 -0800678 def _createSuiteWithMockedTestsAndControlFiles(self):
679 """Create a Suite, using mocked tests and control file contents.
680
681 @return Suite object, after mocking out behavior needed to create it.
682 """
Chris Masonefef21382012-01-17 11:16:32 -0800683 self.expect_control_file_parsing()
684 self.mox.ReplayAll()
Scott Zawalski9ece6532012-02-28 14:10:47 -0500685 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800686 self.getter, self.afe,
687 self.tko)
Chris Masonefef21382012-01-17 11:16:32 -0800688 self.mox.ResetAll()
689 return suite
690
691
Chris Masoneed356392012-05-08 14:07:13 -0700692 def schedule_and_expect_these_results(self, suite, results, recorder):
693 self.mox.StubOutWithMock(suite, 'schedule')
694 suite.schedule(True)
Chris Masone275ec902012-07-10 15:28:34 -0700695 self.manager.unlock()
Chris Masone6fed6462011-10-20 16:36:43 -0700696 for result in results:
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500697 status = result[0]
Chris Masone99378582012-04-30 13:10:58 -0700698 test_name = result[1]
699 recorder.record_entry(
700 StatusContains.CreateFromStrings('START', test_name))
701 recorder.record_entry(
702 StatusContains.CreateFromStrings(*result)).InAnyOrder('results')
703 recorder.record_entry(
704 StatusContains.CreateFromStrings('END %s' % status, test_name))
Chris Masone8d6e6412012-06-28 11:20:56 -0700705 self.mox.StubOutWithMock(job_status, 'wait_for_results')
706 job_status.wait_for_results(self.afe, self.tko, suite._jobs).AndReturn(
707 map(lambda r: job_status.Status(*r), results))
Chris Masoneed356392012-05-08 14:07:13 -0700708
709
710 def testRunAndWaitSuccess(self):
711 """Should record successful results."""
712 suite = self._createSuiteWithMockedTestsAndControlFiles()
713
714 recorder = self.mox.CreateMock(base_job.base_job)
715 recorder.record_entry(
716 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
717
718 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
719 self.schedule_and_expect_these_results(suite, results, recorder)
720 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700721 self.mox.ReplayAll()
722
Chris Masone275ec902012-07-10 15:28:34 -0700723 suite.run_and_wait(recorder.record_entry, self.manager, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700724
725
726 def testRunAndWaitFailure(self):
727 """Should record failure to gather results."""
Chris Masonefef21382012-01-17 11:16:32 -0800728 suite = self._createSuiteWithMockedTestsAndControlFiles()
729
Chris Masone6fed6462011-10-20 16:36:43 -0700730 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700731 recorder.record_entry(
732 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
733 recorder.record_entry(
734 StatusContains.CreateFromStrings('FAIL', self._TAG, 'waiting'))
Chris Masone6fed6462011-10-20 16:36:43 -0700735
Chris Masone6fed6462011-10-20 16:36:43 -0700736 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone8b7cd422012-02-22 13:16:11 -0800737 suite.schedule(True)
Chris Masone275ec902012-07-10 15:28:34 -0700738 self.manager.unlock()
Chris Masone8d6e6412012-06-28 11:20:56 -0700739 self.mox.StubOutWithMock(job_status, 'wait_for_results')
740 job_status.wait_for_results(mox.IgnoreArg(),
741 mox.IgnoreArg(),
742 mox.IgnoreArg()).AndRaise(
743 Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700744 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700745 self.mox.ReplayAll()
746
Chris Masone275ec902012-07-10 15:28:34 -0700747 suite.run_and_wait(recorder.record_entry, self.manager, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700748
749
750 def testRunAndWaitScheduleFailure(self):
Chris Masonefef21382012-01-17 11:16:32 -0800751 """Should record failure to schedule jobs."""
752 suite = self._createSuiteWithMockedTestsAndControlFiles()
753
Chris Masone6fed6462011-10-20 16:36:43 -0700754 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700755 recorder.record_entry(
756 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
757 recorder.record_entry(
758 StatusContains.CreateFromStrings('FAIL', self._TAG, 'scheduling'))
Chris Masone6fed6462011-10-20 16:36:43 -0700759
Chris Masone6fed6462011-10-20 16:36:43 -0700760 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone99378582012-04-30 13:10:58 -0700761 suite.schedule(True).AndRaise(Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700762 self.expect_control_file_reparsing()
763 self.mox.ReplayAll()
764
Chris Masone275ec902012-07-10 15:28:34 -0700765 suite.run_and_wait(recorder.record_entry, self.manager, True)
Chris Masoneed356392012-05-08 14:07:13 -0700766
767
768 def testRunAndWaitDevServerRacyFailure(self):
769 """Should record discovery of dev server races in listing files."""
770 suite = self._createSuiteWithMockedTestsAndControlFiles()
771
772 recorder = self.mox.CreateMock(base_job.base_job)
773 recorder.record_entry(
774 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
775
776 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
777 self.schedule_and_expect_these_results(suite, results, recorder)
778
779 self.expect_racy_control_file_reparsing(
Chris Masone8d6e6412012-06-28 11:20:56 -0700780 {'new': FakeControlData(self._TAG, '!')})
Chris Masoneed356392012-05-08 14:07:13 -0700781
782 recorder.record_entry(
783 StatusContains.CreateFromStrings('FAIL', self._TAG, 'Dev Server'))
Chris Masone6fed6462011-10-20 16:36:43 -0700784 self.mox.ReplayAll()
785
Chris Masone275ec902012-07-10 15:28:34 -0700786 suite.run_and_wait(recorder.record_entry, self.manager, True)