blob: 476ac51c95846eb3ad86e8b832eda31a8cbe1482 [file] [log] [blame]
Chris Masone6fed6462011-10-20 16:36:43 -07001#!/usr/bin/python
2#
Chris Masoneab3e7332012-02-29 18:54:58 -08003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Chris Masone6fed6462011-10-20 16:36:43 -07004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Unit tests for server/cros/dynamic_suite.py."""
8
9import logging
10import mox
Chris Masoned368cc42012-03-07 15:16:59 -080011import random
Chris Masone6fed6462011-10-20 16:36:43 -070012import shutil
13import tempfile
14import time
15import unittest
16
Chris Masonef8b53062012-05-08 22:14:18 -070017from autotest_lib.client.common_lib import base_job, control_data, error
18from autotest_lib.client.common_lib import global_config
Chris Sosa24b3a022012-07-31 14:27:59 -070019from autotest_lib.client.common_lib.cros import dev_server
Chris Masone47c9e642012-04-25 14:22:18 -070020from autotest_lib.frontend.afe.json_rpc import proxy
Chris Masone6fed6462011-10-20 16:36:43 -070021from autotest_lib.server.cros import control_file_getter, dynamic_suite
Chris Masone275ec902012-07-10 15:28:34 -070022from autotest_lib.server.cros import host_lock_manager, job_status
Chris Masone8d6e6412012-06-28 11:20:56 -070023from autotest_lib.server.cros.dynamic_suite_fakes import FakeControlData
24from autotest_lib.server.cros.dynamic_suite_fakes import FakeHost, FakeJob
Chris Masone6ea0cad2012-07-02 09:43:36 -070025from autotest_lib.server.cros.dynamic_suite_fakes import FakeLabel
Chris Masone6fed6462011-10-20 16:36:43 -070026from autotest_lib.server import frontend
27
Chris Masone5374c672012-03-05 15:11:39 -080028
Chris Masonef63576d2012-06-29 11:13:31 -070029class StatusContains(mox.Comparator):
30 @staticmethod
31 def CreateFromStrings(status=None, test_name=None, reason=None):
32 status_comp = mox.StrContains(status) if status else mox.IgnoreArg()
33 name_comp = mox.StrContains(test_name) if test_name else mox.IgnoreArg()
34 reason_comp = mox.StrContains(reason) if reason else mox.IgnoreArg()
35 return StatusContains(status_comp, name_comp, reason_comp)
36
37
38 def __init__(self, status=mox.IgnoreArg(), test_name=mox.IgnoreArg(),
39 reason=mox.IgnoreArg()):
40 """Initialize.
41
42 Takes mox.Comparator objects to apply to dynamic_suite.Status
43 member variables.
44
45 @param status: status code, e.g. 'INFO', 'START', etc.
46 @param test_name: expected test name.
47 @param reason: expected reason
48 """
49 self._status = status
50 self._test_name = test_name
51 self._reason = reason
52
53
54 def equals(self, rhs):
55 """Check to see if fields match base_job.status_log_entry obj in rhs.
56
57 @param rhs: base_job.status_log_entry object to match.
58 @return boolean
59 """
60 return (self._status.equals(rhs.status_code) and
61 self._test_name.equals(rhs.operation) and
62 self._reason.equals(rhs.message))
63
64
65 def __repr__(self):
66 return '<Status containing \'%s\t%s\t%s\'>' % (self._status,
67 self._test_name,
68 self._reason)
69
70
Chris Masoneab3e7332012-02-29 18:54:58 -080071class DynamicSuiteTest(mox.MoxTestBase):
72 """Unit tests for dynamic_suite module methods.
73
74 @var _DARGS: default args to vet.
75 """
76
77
78 def setUp(self):
79 super(DynamicSuiteTest, self).setUp()
80 self._DARGS = {'name': 'name',
81 'build': 'build',
82 'board': 'board',
83 'job': self.mox.CreateMock(base_job.base_job),
84 'num': 1,
85 'pool': 'pool',
86 'skip_reimage': True,
Chris Masone62579122012-03-08 15:18:43 -080087 'check_hosts': False,
Chris Masoneab3e7332012-02-29 18:54:58 -080088 'add_experimental': False}
89
90
91 def testVetRequiredReimageAndRunArgs(self):
92 """Should verify only that required args are present and correct."""
Chris Sosa24b3a022012-07-31 14:27:59 -070093 build, board, name, job, _, _, _, _, _ = \
Chris Masoneab3e7332012-02-29 18:54:58 -080094 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
95 self.assertEquals(build, self._DARGS['build'])
96 self.assertEquals(board, self._DARGS['board'])
97 self.assertEquals(name, self._DARGS['name'])
98 self.assertEquals(job, self._DARGS['job'])
99
100
101 def testVetReimageAndRunBuildArgFail(self):
102 """Should fail verification because |build| arg is bad."""
103 self._DARGS['build'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700104 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -0800105 dynamic_suite._vet_reimage_and_run_args,
106 **self._DARGS)
107
108
109 def testVetReimageAndRunBoardArgFail(self):
110 """Should fail verification because |board| arg is bad."""
111 self._DARGS['board'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700112 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -0800113 dynamic_suite._vet_reimage_and_run_args,
114 **self._DARGS)
115
116
117 def testVetReimageAndRunNameArgFail(self):
118 """Should fail verification because |name| arg is bad."""
119 self._DARGS['name'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700120 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -0800121 dynamic_suite._vet_reimage_and_run_args,
122 **self._DARGS)
123
124
125 def testVetReimageAndRunJobArgFail(self):
126 """Should fail verification because |job| arg is bad."""
127 self._DARGS['job'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700128 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -0800129 dynamic_suite._vet_reimage_and_run_args,
130 **self._DARGS)
131
132
133 def testOverrideOptionalReimageAndRunArgs(self):
134 """Should verify that optional args can be overridden."""
Chris Masone62579122012-03-08 15:18:43 -0800135 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800136 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
137 self.assertEquals(pool, self._DARGS['pool'])
138 self.assertEquals(num, self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800139 self.assertEquals(check, self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800140 self.assertEquals(skip, self._DARGS['skip_reimage'])
141 self.assertEquals(expr, self._DARGS['add_experimental'])
142
143
144 def testDefaultOptionalReimageAndRunArgs(self):
145 """Should verify that optional args get defaults."""
146 del(self._DARGS['pool'])
147 del(self._DARGS['skip_reimage'])
Chris Masone62579122012-03-08 15:18:43 -0800148 del(self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800149 del(self._DARGS['add_experimental'])
150 del(self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800151 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800152 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
153 self.assertEquals(pool, None)
154 self.assertEquals(num, None)
Chris Masone62579122012-03-08 15:18:43 -0800155 self.assertEquals(check, True)
Chris Masoneab3e7332012-02-29 18:54:58 -0800156 self.assertEquals(skip, False)
157 self.assertEquals(expr, True)
158
159
Chris Masone6fed6462011-10-20 16:36:43 -0700160class ReimagerTest(mox.MoxTestBase):
161 """Unit tests for dynamic_suite.Reimager.
162
163 @var _URL: fake image url
Chris Masone8b7cd422012-02-22 13:16:11 -0800164 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700165 @var _NUM: fake number of machines to run on
166 @var _BOARD: fake board to reimage
167 """
168
Chris Sosa24b3a022012-07-31 14:27:59 -0700169 _DEVSERVER_URL = 'http://nothing:8082'
170 _URL = '%s/%s'
Chris Masone8b7cd422012-02-22 13:16:11 -0800171 _BUILD = 'build'
Chris Masone6fed6462011-10-20 16:36:43 -0700172 _NUM = 4
173 _BOARD = 'board'
Chris Masone5552dd72012-02-15 15:01:04 -0800174 _CONFIG = global_config.global_config
Chris Masone6fed6462011-10-20 16:36:43 -0700175
176
177 def setUp(self):
178 super(ReimagerTest, self).setUp()
179 self.afe = self.mox.CreateMock(frontend.AFE)
180 self.tko = self.mox.CreateMock(frontend.TKO)
Chris Masone275ec902012-07-10 15:28:34 -0700181 self.manager = self.mox.CreateMock(host_lock_manager.HostLockManager)
Chris Masone6fed6462011-10-20 16:36:43 -0700182 self.reimager = dynamic_suite.Reimager('', afe=self.afe, tko=self.tko)
Chris Masone5552dd72012-02-15 15:01:04 -0800183 self._CONFIG.override_config_value('CROS',
184 'sharding_factor',
185 "%d" % self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700186
187
188 def testEnsureVersionLabelAlreadyExists(self):
Chris Masone47c9e642012-04-25 14:22:18 -0700189 """Should tolerate a label that already exists."""
Chris Masone6fed6462011-10-20 16:36:43 -0700190 name = 'label'
Chris Masone47c9e642012-04-25 14:22:18 -0700191 error = proxy.ValidationError(
192 {'name': 'ValidationError',
193 'message': '{"name": "This value must be unique"}',
194 'traceback': ''},
195 'BAD')
196 self.afe.create_label(name=name).AndRaise(error)
Chris Masone6fed6462011-10-20 16:36:43 -0700197 self.mox.ReplayAll()
198 self.reimager._ensure_version_label(name)
199
200
201 def testEnsureVersionLabel(self):
202 """Should create a label if it doesn't already exist."""
203 name = 'label'
Chris Masone6fed6462011-10-20 16:36:43 -0700204 self.afe.create_label(name=name)
205 self.mox.ReplayAll()
206 self.reimager._ensure_version_label(name)
207
208
Chris Masone5374c672012-03-05 15:11:39 -0800209 def testCountHostsByBoardAndPool(self):
210 """Should count available hosts by board and pool."""
211 spec = [self._BOARD, 'pool:bvt']
212 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()])
213 self.mox.ReplayAll()
214 self.assertEquals(self.reimager._count_usable_hosts(spec), 1)
215
216
217 def testCountHostsByBoard(self):
218 """Should count available hosts by board."""
219 spec = [self._BOARD]
220 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()] * 2)
221 self.mox.ReplayAll()
222 self.assertEquals(self.reimager._count_usable_hosts(spec), 2)
223
224
225 def testCountZeroHostsByBoard(self):
226 """Should count the available hosts, by board, getting zero."""
227 spec = [self._BOARD]
228 self.afe.get_hosts(multiple_labels=spec).AndReturn([])
229 self.mox.ReplayAll()
230 self.assertEquals(self.reimager._count_usable_hosts(spec), 0)
231
232
Chris Masone6fed6462011-10-20 16:36:43 -0700233 def testInjectVars(self):
234 """Should inject dict of varibles into provided strings."""
235 def find_all_in(d, s):
236 """Returns true if all key-value pairs in |d| are printed in |s|."""
Chris Sosa24b3a022012-07-31 14:27:59 -0700237 for k, v in d.iteritems():
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800238 if isinstance(v, str):
Chris Sosa24b3a022012-07-31 14:27:59 -0700239 if "%s='%s'\n" % (k, v) not in s:
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800240 return False
241 else:
Chris Sosa24b3a022012-07-31 14:27:59 -0700242 if "%s=%r\n" % (k, v) not in s:
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800243 return False
244 return True
Chris Masone6fed6462011-10-20 16:36:43 -0700245
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800246 v = {'v1': 'one', 'v2': 'two', 'v3': None, 'v4': False, 'v5': 5}
Chris Masone8b764252012-01-17 11:12:51 -0800247 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, '')))
248 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, 'ctrl')))
Chris Masone6fed6462011-10-20 16:36:43 -0700249
250
Chris Masone6fed6462011-10-20 16:36:43 -0700251 def testScheduleJob(self):
252 """Should be able to create a job with the AFE."""
253 # Fake out getting the autoupdate control file contents.
254 cf_getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
255 cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
256 self.reimager._cf_getter = cf_getter
Chris Sosa24b3a022012-07-31 14:27:59 -0700257 self._CONFIG.override_config_value('CROS',
258 'dev_server',
259 self._DEVSERVER_URL)
Chris Masone6dca10a2012-02-15 15:41:42 -0800260 self._CONFIG.override_config_value('CROS',
261 'image_url_pattern',
262 self._URL)
Chris Masone6fed6462011-10-20 16:36:43 -0700263 self.afe.create_job(
Chris Sosa24b3a022012-07-31 14:27:59 -0700264 control_file=mox.And(
265 mox.StrContains(self._BUILD),
266 mox.StrContains(self._URL % (self._DEVSERVER_URL,
267 self._BUILD))),
Chris Masone8b7cd422012-02-22 13:16:11 -0800268 name=mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700269 control_type='Server',
Chris Masone5374c672012-03-05 15:11:39 -0800270 meta_hosts=[self._BOARD] * self._NUM,
Chris Masone99378582012-04-30 13:10:58 -0700271 dependencies=[],
272 priority='Low')
Chris Masone6fed6462011-10-20 16:36:43 -0700273 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700274 self.reimager._schedule_reimage_job(self._BUILD, self._BOARD, None,
275 self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700276
Chris Sosa24b3a022012-07-31 14:27:59 -0700277 def testPackageUrl(self):
278 """Should be able to get the package_url for any build."""
279 self._CONFIG.override_config_value('CROS',
280 'dev_server',
281 self._DEVSERVER_URL)
282 self._CONFIG.override_config_value('CROS',
283 'package_url_pattern',
284 self._URL)
285 self.mox.ReplayAll()
286 package_url = dynamic_suite.get_package_url(self._BUILD)
287 self.assertEqual(package_url, self._URL % (self._DEVSERVER_URL,
288 self._BUILD))
Chris Masone6fed6462011-10-20 16:36:43 -0700289
Chris Masone275ec902012-07-10 15:28:34 -0700290 def expect_attempt(self, canary_job, statuses, ex=None, check_hosts=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700291 """Sets up |self.reimager| to expect an attempt() that returns |success|
292
Chris Masone6ea0cad2012-07-02 09:43:36 -0700293 Also stubs out Reimager._clear_build_state(), should the caller wish
Chris Masoned368cc42012-03-07 15:16:59 -0800294 to set an expectation there as well.
295
Chris Masone275ec902012-07-10 15:28:34 -0700296 @param canary_job: a FakeJob representing the job we're expecting.
297 @param statuses: dict mapping a hostname to its job_status.Status.
298 Will be returned by job_status.gather_per_host_results
Chris Masone796fcf12012-02-22 16:53:31 -0800299 @param ex: if not None, |ex| is raised by get_jobs()
Chris Masone6fed6462011-10-20 16:36:43 -0700300 @return a FakeJob configured with appropriate expectations
301 """
Chris Masone6fed6462011-10-20 16:36:43 -0700302 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
Chris Masone6fed6462011-10-20 16:36:43 -0700303 self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
Chris Masone275ec902012-07-10 15:28:34 -0700304 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
305 self.mox.StubOutWithMock(self.reimager, '_clear_build_state')
306
Chris Masone517ef482012-07-23 15:36:36 -0700307 self.mox.StubOutWithMock(job_status, 'wait_for_jobs_to_start')
Chris Masone275ec902012-07-10 15:28:34 -0700308 self.mox.StubOutWithMock(job_status, 'wait_for_and_lock_job_hosts')
309 self.mox.StubOutWithMock(job_status, 'gather_job_hostnames')
Chris Masone517ef482012-07-23 15:36:36 -0700310 self.mox.StubOutWithMock(job_status, 'wait_for_jobs_to_finish')
Chris Masone275ec902012-07-10 15:28:34 -0700311 self.mox.StubOutWithMock(job_status, 'gather_per_host_results')
312 self.mox.StubOutWithMock(job_status, 'record_and_report_results')
313
Chris Masone275ec902012-07-10 15:28:34 -0700314 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone8b7cd422012-02-22 13:16:11 -0800315 self.reimager._schedule_reimage_job(self._BUILD,
Chris Masonec43448f2012-05-31 12:55:59 -0700316 self._BOARD,
317 None,
Chris Masone275ec902012-07-10 15:28:34 -0700318 self._NUM).AndReturn(canary_job)
Chris Masone62579122012-03-08 15:18:43 -0800319 if check_hosts:
Chris Masone62579122012-03-08 15:18:43 -0800320 self.reimager._count_usable_hosts(
321 mox.IgnoreArg()).AndReturn(self._NUM)
Chris Masone5374c672012-03-05 15:11:39 -0800322
Chris Masone517ef482012-07-23 15:36:36 -0700323 job_status.wait_for_jobs_to_start(self.afe, [canary_job])
Chris Masone275ec902012-07-10 15:28:34 -0700324 job_status.wait_for_and_lock_job_hosts(
Chris Masone517ef482012-07-23 15:36:36 -0700325 self.afe, [canary_job], self.manager).AndReturn(statuses.keys())
Chris Masone275ec902012-07-10 15:28:34 -0700326
327 if ex:
Chris Masone517ef482012-07-23 15:36:36 -0700328 job_status.wait_for_jobs_to_finish(self.afe,
329 [canary_job]).AndRaise(ex)
Chris Masone796fcf12012-02-22 16:53:31 -0800330 else:
Chris Masone517ef482012-07-23 15:36:36 -0700331 job_status.wait_for_jobs_to_finish(self.afe, [canary_job])
Chris Masone6ea0cad2012-07-02 09:43:36 -0700332 job_status.gather_per_host_results(
Chris Masone275ec902012-07-10 15:28:34 -0700333 mox.IgnoreArg(), mox.IgnoreArg(), [canary_job],
Chris Masone6ea0cad2012-07-02 09:43:36 -0700334 mox.StrContains(dynamic_suite.REIMAGE_JOB_NAME)).AndReturn(
335 statuses)
336
337 if statuses:
Chris Sosa24b3a022012-07-31 14:27:59 -0700338 ret_val = reduce(lambda v, s: v and s.is_good(),
Chris Masone6ea0cad2012-07-02 09:43:36 -0700339 statuses.values(), True)
Chris Masone6ea0cad2012-07-02 09:43:36 -0700340 job_status.record_and_report_results(
341 statuses.values(), mox.IgnoreArg()).AndReturn(ret_val)
Chris Masone6fed6462011-10-20 16:36:43 -0700342
Chris Masone6fed6462011-10-20 16:36:43 -0700343
344 def testSuccessfulReimage(self):
345 """Should attempt a reimage and record success."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700346 canary = FakeJob()
Chris Masone517ef482012-07-23 15:36:36 -0700347 statuses = {canary.hostnames[0]: job_status.Status('GOOD',
348 canary.hostnames[0])}
Chris Masone6ea0cad2012-07-02 09:43:36 -0700349 self.expect_attempt(canary, statuses)
Chris Masone6fed6462011-10-20 16:36:43 -0700350
351 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone517ef482012-07-23 15:36:36 -0700352 self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
Chris Masone6fed6462011-10-20 16:36:43 -0700353 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700354 self.assertTrue(self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700355 rjob.record_entry, True,
356 self.manager))
Chris Masoned368cc42012-03-07 15:16:59 -0800357 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700358
359
360 def testFailedReimage(self):
361 """Should attempt a reimage and record failure."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700362 canary = FakeJob()
Chris Masone517ef482012-07-23 15:36:36 -0700363 statuses = {canary.hostnames[0]: job_status.Status('FAIL',
364 canary.hostnames[0])}
Chris Masone6ea0cad2012-07-02 09:43:36 -0700365 self.expect_attempt(canary, statuses)
Chris Masone6fed6462011-10-20 16:36:43 -0700366
367 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone517ef482012-07-23 15:36:36 -0700368 self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
Chris Masone6fed6462011-10-20 16:36:43 -0700369 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700370 self.assertFalse(self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700371 rjob.record_entry, True,
372 self.manager))
Chris Masoned368cc42012-03-07 15:16:59 -0800373 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700374
375
376 def testReimageThatNeverHappened(self):
377 """Should attempt a reimage and record that it didn't run."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700378 canary = FakeJob()
379 statuses = {'hostless': job_status.Status('ABORT', 'big_job_name')}
380 self.expect_attempt(canary, statuses)
Chris Masone6fed6462011-10-20 16:36:43 -0700381
382 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone6fed6462011-10-20 16:36:43 -0700383 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700384 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700385 rjob.record_entry, True, self.manager)
Chris Masoned368cc42012-03-07 15:16:59 -0800386 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700387
388
Chris Masone796fcf12012-02-22 16:53:31 -0800389 def testReimageThatRaised(self):
390 """Should attempt a reimage that raises an exception and record that."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700391 canary = FakeJob()
Chris Masone796fcf12012-02-22 16:53:31 -0800392 ex_message = 'Oh no!'
Chris Masone6ea0cad2012-07-02 09:43:36 -0700393 self.expect_attempt(canary, statuses={}, ex=Exception(ex_message))
Chris Masone796fcf12012-02-22 16:53:31 -0800394
395 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700396 rjob.record_entry(StatusContains.CreateFromStrings('START'))
397 rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
398 reason=ex_message))
399 rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
Chris Masone796fcf12012-02-22 16:53:31 -0800400 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700401 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700402 rjob.record_entry, True, self.manager)
Chris Masone62579122012-03-08 15:18:43 -0800403 self.reimager.clear_reimaged_host_state(self._BUILD)
404
405
406 def testSuccessfulReimageThatCouldNotScheduleRightAway(self):
Chris Masone99378582012-04-30 13:10:58 -0700407 """Should attempt reimage, ignoring host availability; record success.
Chris Masone62579122012-03-08 15:18:43 -0800408 """
Chris Masone6ea0cad2012-07-02 09:43:36 -0700409 canary = FakeJob()
Chris Masone517ef482012-07-23 15:36:36 -0700410 statuses = {canary.hostnames[0]: job_status.Status('GOOD',
411 canary.hostnames[0])}
Chris Masone6ea0cad2012-07-02 09:43:36 -0700412 self.expect_attempt(canary, statuses, check_hosts=False)
Chris Masone62579122012-03-08 15:18:43 -0800413
414 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone517ef482012-07-23 15:36:36 -0700415 self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
Chris Masone62579122012-03-08 15:18:43 -0800416 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700417 self.assertTrue(self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700418 rjob.record_entry, False,
419 self.manager))
Chris Masoned368cc42012-03-07 15:16:59 -0800420 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone796fcf12012-02-22 16:53:31 -0800421
422
Chris Masone5374c672012-03-05 15:11:39 -0800423 def testReimageThatCouldNotSchedule(self):
424 """Should attempt a reimage that can't be scheduled."""
Chris Masone502b71e2012-04-10 10:41:35 -0700425 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
426 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
427
428 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
429 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(1)
430
431 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700432 rjob.record_entry(StatusContains.CreateFromStrings('START'))
433 rjob.record_entry(
434 StatusContains.CreateFromStrings('WARN', reason='Too few hosts'))
435 rjob.record_entry(StatusContains.CreateFromStrings('END WARN'))
Chris Masone502b71e2012-04-10 10:41:35 -0700436 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700437 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700438 rjob.record_entry, True, self.manager)
Chris Masone502b71e2012-04-10 10:41:35 -0700439 self.reimager.clear_reimaged_host_state(self._BUILD)
440
441
442 def testReimageWithNoAvailableHosts(self):
443 """Should attempt a reimage while all hosts are dead."""
Chris Masone62579122012-03-08 15:18:43 -0800444 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
445 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone5374c672012-03-05 15:11:39 -0800446
447 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
448 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(0)
449
450 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700451 rjob.record_entry(StatusContains.CreateFromStrings('START'))
452 rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
453 reason='All hosts'))
454 rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
Chris Masone5374c672012-03-05 15:11:39 -0800455 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700456 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700457 rjob.record_entry, True, self.manager)
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):
Chris Masone6fed6462011-10-20 16:36:43 -0700473 super(SuiteTest, self).setUp()
474 self.afe = self.mox.CreateMock(frontend.AFE)
475 self.tko = self.mox.CreateMock(frontend.TKO)
476
477 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__)
478
Chris Masone275ec902012-07-10 15:28:34 -0700479 self.manager = self.mox.CreateMock(host_lock_manager.HostLockManager)
Chris Masone6fed6462011-10-20 16:36:43 -0700480 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
481
Chris Masone8d6e6412012-06-28 11:20:56 -0700482 self.files = {'one': FakeControlData(self._TAG, 'data_one', expr=True),
483 'two': FakeControlData(self._TAG, 'data_two'),
484 'three': FakeControlData(self._TAG, 'data_three')}
Chris Masone6fed6462011-10-20 16:36:43 -0700485
Chris Masone75a20612012-05-08 12:37:31 -0700486 self.files_to_filter = {
Chris Masone8d6e6412012-06-28 11:20:56 -0700487 'with/deps/...': FakeControlData(self._TAG, 'gets filtered'),
488 'with/profilers/...': FakeControlData(self._TAG, 'gets filtered')}
Chris Masone75a20612012-05-08 12:37:31 -0700489
Chris Masone6fed6462011-10-20 16:36:43 -0700490
491 def tearDown(self):
492 super(SuiteTest, self).tearDown()
493 shutil.rmtree(self.tmpdir, ignore_errors=True)
494
495
496 def expect_control_file_parsing(self):
497 """Expect an attempt to parse the 'control files' in |self.files|."""
Chris Masone75a20612012-05-08 12:37:31 -0700498 all_files = self.files.keys() + self.files_to_filter.keys()
Chris Masoneed356392012-05-08 14:07:13 -0700499 self._set_control_file_parsing_expectations(False, all_files,
500 self.files.iteritems())
501
502
503 def expect_control_file_reparsing(self):
504 """Expect re-parsing the 'control files' in |self.files|."""
505 all_files = self.files.keys() + self.files_to_filter.keys()
506 self._set_control_file_parsing_expectations(True, all_files,
507 self.files.iteritems())
508
509
510 def expect_racy_control_file_reparsing(self, new_files):
511 """Expect re-fetching and parsing of control files to return extra.
512
513 @param new_files: extra control files that showed up during scheduling.
514 """
515 all_files = (self.files.keys() + self.files_to_filter.keys() +
516 new_files.keys())
517 new_files.update(self.files)
518 self._set_control_file_parsing_expectations(True, all_files,
519 new_files.iteritems())
520
521
522 def _set_control_file_parsing_expectations(self, already_stubbed,
523 file_list, files_to_parse):
524 """Expect an attempt to parse the 'control files' in |files|.
525
526 @param already_stubbed: parse_control_string already stubbed out.
527 @param file_list: the files the dev server returns
528 @param files_to_parse: the {'name': FakeControlData} dict of files we
529 expect to get parsed.
530 """
531 if not already_stubbed:
532 self.mox.StubOutWithMock(control_data, 'parse_control_string')
533
534 self.getter.get_control_file_list().AndReturn(file_list)
535 for file, data in files_to_parse:
536 self.getter.get_control_file_contents(
537 file).InAnyOrder().AndReturn(data.string)
Chris Masone6fed6462011-10-20 16:36:43 -0700538 control_data.parse_control_string(
Chris Masoneed356392012-05-08 14:07:13 -0700539 data.string, raise_warnings=True).InAnyOrder().AndReturn(data)
Chris Masone6fed6462011-10-20 16:36:43 -0700540
541
542 def testFindAndParseStableTests(self):
543 """Should find only non-experimental tests that match a predicate."""
544 self.expect_control_file_parsing()
545 self.mox.ReplayAll()
546
547 predicate = lambda d: d.text == self.files['two'].string
548 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter, predicate)
549 self.assertEquals(len(tests), 1)
550 self.assertEquals(tests[0], self.files['two'])
551
552
553 def testFindAndParseTests(self):
554 """Should find all tests that match a predicate."""
555 self.expect_control_file_parsing()
556 self.mox.ReplayAll()
557
558 predicate = lambda d: d.text != self.files['two'].string
559 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter,
560 predicate,
561 add_experimental=True)
562 self.assertEquals(len(tests), 2)
563 self.assertTrue(self.files['one'] in tests)
564 self.assertTrue(self.files['three'] in tests)
565
566
567 def mock_control_file_parsing(self):
568 """Fake out find_and_parse_tests(), returning content from |self.files|.
569 """
570 for test in self.files.values():
571 test.text = test.string # mimic parsing.
572 self.mox.StubOutWithMock(dynamic_suite.Suite, 'find_and_parse_tests')
573 dynamic_suite.Suite.find_and_parse_tests(
574 mox.IgnoreArg(),
575 mox.IgnoreArg(),
576 add_experimental=True).AndReturn(self.files.values())
577
578
579 def testStableUnstableFilter(self):
580 """Should distinguish between experimental and stable tests."""
581 self.mock_control_file_parsing()
582 self.mox.ReplayAll()
583 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
Chris Masoned6f38c82012-02-22 14:53:42 -0800584 afe=self.afe, tko=self.tko)
Chris Masone6fed6462011-10-20 16:36:43 -0700585
586 self.assertTrue(self.files['one'] in suite.tests)
587 self.assertTrue(self.files['two'] in suite.tests)
588 self.assertTrue(self.files['one'] in suite.unstable_tests())
589 self.assertTrue(self.files['two'] in suite.stable_tests())
590 self.assertFalse(self.files['one'] in suite.stable_tests())
591 self.assertFalse(self.files['two'] in suite.unstable_tests())
592
593
594 def expect_job_scheduling(self, add_experimental):
595 """Expect jobs to be scheduled for 'tests' in |self.files|.
596
597 @param add_experimental: expect jobs for experimental tests as well.
598 """
599 for test in self.files.values():
600 if not add_experimental and test.experimental:
601 continue
602 self.afe.create_job(
603 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800604 name=mox.And(mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700605 mox.StrContains(test.name)),
606 control_type=mox.IgnoreArg(),
Chris Masone8b7cd422012-02-22 13:16:11 -0800607 meta_hosts=[dynamic_suite.VERSION_PREFIX + self._BUILD],
Chris Masonebafbbb02012-05-16 13:41:36 -0700608 dependencies=[],
609 keyvals={'build': self._BUILD, 'suite': self._TAG}
610 ).AndReturn(FakeJob())
Chris Masone6fed6462011-10-20 16:36:43 -0700611
612
613 def testScheduleTests(self):
614 """Should schedule stable and experimental tests with the AFE."""
615 self.mock_control_file_parsing()
616 self.expect_job_scheduling(add_experimental=True)
617
618 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800619 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800620 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800621 suite.schedule()
Chris Masone6fed6462011-10-20 16:36:43 -0700622
623
Scott Zawalski9ece6532012-02-28 14:10:47 -0500624 def testScheduleTestsAndRecord(self):
625 """Should schedule stable and experimental tests with the AFE."""
626 self.mock_control_file_parsing()
627 self.mox.ReplayAll()
628 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
629 afe=self.afe, tko=self.tko,
630 results_dir=self.tmpdir)
631 self.mox.ResetAll()
632 self.expect_job_scheduling(add_experimental=True)
633 self.mox.StubOutWithMock(suite, '_record_scheduled_jobs')
634 suite._record_scheduled_jobs()
635 self.mox.ReplayAll()
636 suite.schedule()
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500637 for job in suite._jobs:
638 self.assertTrue(hasattr(job, 'test_name'))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500639
640
Chris Masone6fed6462011-10-20 16:36:43 -0700641 def testScheduleStableTests(self):
642 """Should schedule only stable tests with the AFE."""
643 self.mock_control_file_parsing()
644 self.expect_job_scheduling(add_experimental=False)
645
646 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800647 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800648 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800649 suite.schedule(add_experimental=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700650
651
Chris Masonefef21382012-01-17 11:16:32 -0800652 def _createSuiteWithMockedTestsAndControlFiles(self):
653 """Create a Suite, using mocked tests and control file contents.
654
655 @return Suite object, after mocking out behavior needed to create it.
656 """
Chris Masonefef21382012-01-17 11:16:32 -0800657 self.expect_control_file_parsing()
658 self.mox.ReplayAll()
Scott Zawalski9ece6532012-02-28 14:10:47 -0500659 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800660 self.getter, self.afe,
661 self.tko)
Chris Masonefef21382012-01-17 11:16:32 -0800662 self.mox.ResetAll()
663 return suite
664
665
Chris Masoneed356392012-05-08 14:07:13 -0700666 def schedule_and_expect_these_results(self, suite, results, recorder):
667 self.mox.StubOutWithMock(suite, 'schedule')
668 suite.schedule(True)
Chris Masone275ec902012-07-10 15:28:34 -0700669 self.manager.unlock()
Chris Masone6fed6462011-10-20 16:36:43 -0700670 for result in results:
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500671 status = result[0]
Chris Masone99378582012-04-30 13:10:58 -0700672 test_name = result[1]
673 recorder.record_entry(
674 StatusContains.CreateFromStrings('START', test_name))
675 recorder.record_entry(
676 StatusContains.CreateFromStrings(*result)).InAnyOrder('results')
677 recorder.record_entry(
678 StatusContains.CreateFromStrings('END %s' % status, test_name))
Chris Masone8d6e6412012-06-28 11:20:56 -0700679 self.mox.StubOutWithMock(job_status, 'wait_for_results')
680 job_status.wait_for_results(self.afe, self.tko, suite._jobs).AndReturn(
681 map(lambda r: job_status.Status(*r), results))
Chris Masoneed356392012-05-08 14:07:13 -0700682
683
684 def testRunAndWaitSuccess(self):
685 """Should record successful results."""
686 suite = self._createSuiteWithMockedTestsAndControlFiles()
687
688 recorder = self.mox.CreateMock(base_job.base_job)
689 recorder.record_entry(
690 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
691
692 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
693 self.schedule_and_expect_these_results(suite, results, recorder)
694 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700695 self.mox.ReplayAll()
696
Chris Masone275ec902012-07-10 15:28:34 -0700697 suite.run_and_wait(recorder.record_entry, self.manager, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700698
699
700 def testRunAndWaitFailure(self):
701 """Should record failure to gather results."""
Chris Masonefef21382012-01-17 11:16:32 -0800702 suite = self._createSuiteWithMockedTestsAndControlFiles()
703
Chris Masone6fed6462011-10-20 16:36:43 -0700704 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700705 recorder.record_entry(
706 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
707 recorder.record_entry(
708 StatusContains.CreateFromStrings('FAIL', self._TAG, 'waiting'))
Chris Masone6fed6462011-10-20 16:36:43 -0700709
Chris Masone6fed6462011-10-20 16:36:43 -0700710 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone8b7cd422012-02-22 13:16:11 -0800711 suite.schedule(True)
Chris Masone275ec902012-07-10 15:28:34 -0700712 self.manager.unlock()
Chris Masone8d6e6412012-06-28 11:20:56 -0700713 self.mox.StubOutWithMock(job_status, 'wait_for_results')
714 job_status.wait_for_results(mox.IgnoreArg(),
715 mox.IgnoreArg(),
716 mox.IgnoreArg()).AndRaise(
717 Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700718 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700719 self.mox.ReplayAll()
720
Chris Masone275ec902012-07-10 15:28:34 -0700721 suite.run_and_wait(recorder.record_entry, self.manager, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700722
723
724 def testRunAndWaitScheduleFailure(self):
Chris Masonefef21382012-01-17 11:16:32 -0800725 """Should record failure to schedule jobs."""
726 suite = self._createSuiteWithMockedTestsAndControlFiles()
727
Chris Masone6fed6462011-10-20 16:36:43 -0700728 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700729 recorder.record_entry(
730 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
731 recorder.record_entry(
732 StatusContains.CreateFromStrings('FAIL', self._TAG, 'scheduling'))
Chris Masone6fed6462011-10-20 16:36:43 -0700733
Chris Masone6fed6462011-10-20 16:36:43 -0700734 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone99378582012-04-30 13:10:58 -0700735 suite.schedule(True).AndRaise(Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700736 self.expect_control_file_reparsing()
737 self.mox.ReplayAll()
738
Chris Masone275ec902012-07-10 15:28:34 -0700739 suite.run_and_wait(recorder.record_entry, self.manager, True)
Chris Masoneed356392012-05-08 14:07:13 -0700740
741
742 def testRunAndWaitDevServerRacyFailure(self):
743 """Should record discovery of dev server races in listing files."""
744 suite = self._createSuiteWithMockedTestsAndControlFiles()
745
746 recorder = self.mox.CreateMock(base_job.base_job)
747 recorder.record_entry(
748 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
749
750 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
751 self.schedule_and_expect_these_results(suite, results, recorder)
752
753 self.expect_racy_control_file_reparsing(
Chris Masone8d6e6412012-06-28 11:20:56 -0700754 {'new': FakeControlData(self._TAG, '!')})
Chris Masoneed356392012-05-08 14:07:13 -0700755
756 recorder.record_entry(
757 StatusContains.CreateFromStrings('FAIL', self._TAG, 'Dev Server'))
Chris Masone6fed6462011-10-20 16:36:43 -0700758 self.mox.ReplayAll()
759
Chris Masone275ec902012-07-10 15:28:34 -0700760 suite.run_and_wait(recorder.record_entry, self.manager, True)