blob: 5efd76f690ac52662d9f85110f716d89ce4493cb [file] [log] [blame]
Chris Masone6fed6462011-10-20 16:36:43 -07001#!/usr/bin/python
2#
Chris Masoneab3e7332012-02-29 18:54:58 -08003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Chris Masone6fed6462011-10-20 16:36:43 -07004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Unit tests for server/cros/dynamic_suite.py."""
8
9import logging
10import mox
Chris Masoned368cc42012-03-07 15:16:59 -080011import random
Chris Masone6fed6462011-10-20 16:36:43 -070012import shutil
13import tempfile
14import time
15import unittest
16
Chris Masonef8b53062012-05-08 22:14:18 -070017from autotest_lib.client.common_lib import base_job, control_data, error
18from autotest_lib.client.common_lib import global_config
Chris Masone47c9e642012-04-25 14:22:18 -070019from autotest_lib.frontend.afe.json_rpc import proxy
Chris Masone6fed6462011-10-20 16:36:43 -070020from autotest_lib.server.cros import control_file_getter, dynamic_suite
Chris Masone275ec902012-07-10 15:28:34 -070021from autotest_lib.server.cros import host_lock_manager, job_status
Chris Masone8d6e6412012-06-28 11:20:56 -070022from autotest_lib.server.cros.dynamic_suite_fakes import FakeControlData
23from autotest_lib.server.cros.dynamic_suite_fakes import FakeHost, FakeJob
Chris Masone6ea0cad2012-07-02 09:43:36 -070024from autotest_lib.server.cros.dynamic_suite_fakes import FakeLabel
Chris Masone6fed6462011-10-20 16:36:43 -070025from autotest_lib.server import frontend
26
Chris Masone5374c672012-03-05 15:11:39 -080027
Chris Masonef63576d2012-06-29 11:13:31 -070028class StatusContains(mox.Comparator):
29 @staticmethod
30 def CreateFromStrings(status=None, test_name=None, reason=None):
31 status_comp = mox.StrContains(status) if status else mox.IgnoreArg()
32 name_comp = mox.StrContains(test_name) if test_name else mox.IgnoreArg()
33 reason_comp = mox.StrContains(reason) if reason else mox.IgnoreArg()
34 return StatusContains(status_comp, name_comp, reason_comp)
35
36
37 def __init__(self, status=mox.IgnoreArg(), test_name=mox.IgnoreArg(),
38 reason=mox.IgnoreArg()):
39 """Initialize.
40
41 Takes mox.Comparator objects to apply to dynamic_suite.Status
42 member variables.
43
44 @param status: status code, e.g. 'INFO', 'START', etc.
45 @param test_name: expected test name.
46 @param reason: expected reason
47 """
48 self._status = status
49 self._test_name = test_name
50 self._reason = reason
51
52
53 def equals(self, rhs):
54 """Check to see if fields match base_job.status_log_entry obj in rhs.
55
56 @param rhs: base_job.status_log_entry object to match.
57 @return boolean
58 """
59 return (self._status.equals(rhs.status_code) and
60 self._test_name.equals(rhs.operation) and
61 self._reason.equals(rhs.message))
62
63
64 def __repr__(self):
65 return '<Status containing \'%s\t%s\t%s\'>' % (self._status,
66 self._test_name,
67 self._reason)
68
69
Chris Masoneab3e7332012-02-29 18:54:58 -080070class DynamicSuiteTest(mox.MoxTestBase):
71 """Unit tests for dynamic_suite module methods.
72
73 @var _DARGS: default args to vet.
74 """
75
76
77 def setUp(self):
78 super(DynamicSuiteTest, self).setUp()
79 self._DARGS = {'name': 'name',
80 'build': 'build',
81 'board': 'board',
82 'job': self.mox.CreateMock(base_job.base_job),
83 'num': 1,
84 'pool': 'pool',
85 'skip_reimage': True,
Chris Masone62579122012-03-08 15:18:43 -080086 'check_hosts': False,
Chris Masoneab3e7332012-02-29 18:54:58 -080087 'add_experimental': False}
88
89
90 def testVetRequiredReimageAndRunArgs(self):
91 """Should verify only that required args are present and correct."""
Chris Sosa5ca9d1b2012-07-30 14:41:18 -070092 build, board, name, job, _, _, _, _,_ = \
Chris Masoneab3e7332012-02-29 18:54:58 -080093 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
94 self.assertEquals(build, self._DARGS['build'])
95 self.assertEquals(board, self._DARGS['board'])
96 self.assertEquals(name, self._DARGS['name'])
97 self.assertEquals(job, self._DARGS['job'])
98
99
100 def testVetReimageAndRunBuildArgFail(self):
101 """Should fail verification because |build| arg is bad."""
102 self._DARGS['build'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700103 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -0800104 dynamic_suite._vet_reimage_and_run_args,
105 **self._DARGS)
106
107
108 def testVetReimageAndRunBoardArgFail(self):
109 """Should fail verification because |board| arg is bad."""
110 self._DARGS['board'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700111 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -0800112 dynamic_suite._vet_reimage_and_run_args,
113 **self._DARGS)
114
115
116 def testVetReimageAndRunNameArgFail(self):
117 """Should fail verification because |name| arg is bad."""
118 self._DARGS['name'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700119 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -0800120 dynamic_suite._vet_reimage_and_run_args,
121 **self._DARGS)
122
123
124 def testVetReimageAndRunJobArgFail(self):
125 """Should fail verification because |job| arg is bad."""
126 self._DARGS['job'] = None
Chris Masonef8b53062012-05-08 22:14:18 -0700127 self.assertRaises(error.SuiteArgumentException,
Chris Masoneab3e7332012-02-29 18:54:58 -0800128 dynamic_suite._vet_reimage_and_run_args,
129 **self._DARGS)
130
131
132 def testOverrideOptionalReimageAndRunArgs(self):
133 """Should verify that optional args can be overridden."""
Chris Masone62579122012-03-08 15:18:43 -0800134 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800135 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
136 self.assertEquals(pool, self._DARGS['pool'])
137 self.assertEquals(num, self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800138 self.assertEquals(check, self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800139 self.assertEquals(skip, self._DARGS['skip_reimage'])
140 self.assertEquals(expr, self._DARGS['add_experimental'])
141
142
143 def testDefaultOptionalReimageAndRunArgs(self):
144 """Should verify that optional args get defaults."""
145 del(self._DARGS['pool'])
146 del(self._DARGS['skip_reimage'])
Chris Masone62579122012-03-08 15:18:43 -0800147 del(self._DARGS['check_hosts'])
Chris Masoneab3e7332012-02-29 18:54:58 -0800148 del(self._DARGS['add_experimental'])
149 del(self._DARGS['num'])
Chris Masone62579122012-03-08 15:18:43 -0800150 _, _, _, _, pool, num, check, skip, expr = \
Chris Masoneab3e7332012-02-29 18:54:58 -0800151 dynamic_suite._vet_reimage_and_run_args(**self._DARGS)
152 self.assertEquals(pool, None)
153 self.assertEquals(num, None)
Chris Masone62579122012-03-08 15:18:43 -0800154 self.assertEquals(check, True)
Chris Masoneab3e7332012-02-29 18:54:58 -0800155 self.assertEquals(skip, False)
156 self.assertEquals(expr, True)
157
158
Chris Masone6fed6462011-10-20 16:36:43 -0700159class ReimagerTest(mox.MoxTestBase):
160 """Unit tests for dynamic_suite.Reimager.
161
162 @var _URL: fake image url
Chris Masone8b7cd422012-02-22 13:16:11 -0800163 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700164 @var _NUM: fake number of machines to run on
165 @var _BOARD: fake board to reimage
166 """
167
Chris Sosa5ca9d1b2012-07-30 14:41:18 -0700168 _URL = 'http://nothing/%s'
Chris Masone8b7cd422012-02-22 13:16:11 -0800169 _BUILD = 'build'
Chris Masone6fed6462011-10-20 16:36:43 -0700170 _NUM = 4
171 _BOARD = 'board'
Chris Masone5552dd72012-02-15 15:01:04 -0800172 _CONFIG = global_config.global_config
Chris Masone6fed6462011-10-20 16:36:43 -0700173
174
175 def setUp(self):
176 super(ReimagerTest, self).setUp()
177 self.afe = self.mox.CreateMock(frontend.AFE)
178 self.tko = self.mox.CreateMock(frontend.TKO)
Chris Masone275ec902012-07-10 15:28:34 -0700179 self.manager = self.mox.CreateMock(host_lock_manager.HostLockManager)
Chris Masone6fed6462011-10-20 16:36:43 -0700180 self.reimager = dynamic_suite.Reimager('', afe=self.afe, tko=self.tko)
Chris Masone5552dd72012-02-15 15:01:04 -0800181 self._CONFIG.override_config_value('CROS',
182 'sharding_factor',
183 "%d" % self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700184
185
186 def testEnsureVersionLabelAlreadyExists(self):
Chris Masone47c9e642012-04-25 14:22:18 -0700187 """Should tolerate a label that already exists."""
Chris Masone6fed6462011-10-20 16:36:43 -0700188 name = 'label'
Chris Masone47c9e642012-04-25 14:22:18 -0700189 error = proxy.ValidationError(
190 {'name': 'ValidationError',
191 'message': '{"name": "This value must be unique"}',
192 'traceback': ''},
193 'BAD')
194 self.afe.create_label(name=name).AndRaise(error)
Chris Masone6fed6462011-10-20 16:36:43 -0700195 self.mox.ReplayAll()
196 self.reimager._ensure_version_label(name)
197
198
199 def testEnsureVersionLabel(self):
200 """Should create a label if it doesn't already exist."""
201 name = 'label'
Chris Masone6fed6462011-10-20 16:36:43 -0700202 self.afe.create_label(name=name)
203 self.mox.ReplayAll()
204 self.reimager._ensure_version_label(name)
205
206
Chris Masone5374c672012-03-05 15:11:39 -0800207 def testCountHostsByBoardAndPool(self):
208 """Should count available hosts by board and pool."""
209 spec = [self._BOARD, 'pool:bvt']
210 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()])
211 self.mox.ReplayAll()
212 self.assertEquals(self.reimager._count_usable_hosts(spec), 1)
213
214
215 def testCountHostsByBoard(self):
216 """Should count available hosts by board."""
217 spec = [self._BOARD]
218 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()] * 2)
219 self.mox.ReplayAll()
220 self.assertEquals(self.reimager._count_usable_hosts(spec), 2)
221
222
223 def testCountZeroHostsByBoard(self):
224 """Should count the available hosts, by board, getting zero."""
225 spec = [self._BOARD]
226 self.afe.get_hosts(multiple_labels=spec).AndReturn([])
227 self.mox.ReplayAll()
228 self.assertEquals(self.reimager._count_usable_hosts(spec), 0)
229
230
Chris Masone6fed6462011-10-20 16:36:43 -0700231 def testInjectVars(self):
232 """Should inject dict of varibles into provided strings."""
233 def find_all_in(d, s):
234 """Returns true if all key-value pairs in |d| are printed in |s|."""
Chris Sosa5ca9d1b2012-07-30 14:41:18 -0700235 for k,v in d.iteritems():
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800236 if isinstance(v, str):
Chris Sosa5ca9d1b2012-07-30 14:41:18 -0700237 if "%s='%s'\n" % (k,v) not in s:
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800238 return False
239 else:
Chris Sosa5ca9d1b2012-07-30 14:41:18 -0700240 if "%s=%r\n" % (k,v) not in s:
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800241 return False
242 return True
Chris Masone6fed6462011-10-20 16:36:43 -0700243
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800244 v = {'v1': 'one', 'v2': 'two', 'v3': None, 'v4': False, 'v5': 5}
Chris Masone8b764252012-01-17 11:12:51 -0800245 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, '')))
246 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, 'ctrl')))
Chris Masone6fed6462011-10-20 16:36:43 -0700247
248
Chris Masone6fed6462011-10-20 16:36:43 -0700249 def testScheduleJob(self):
250 """Should be able to create a job with the AFE."""
251 # Fake out getting the autoupdate control file contents.
252 cf_getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
253 cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
254 self.reimager._cf_getter = cf_getter
Chris Sosa5ca9d1b2012-07-30 14:41:18 -0700255
Chris Masone6dca10a2012-02-15 15:41:42 -0800256 self._CONFIG.override_config_value('CROS',
257 'image_url_pattern',
258 self._URL)
Chris Masone6fed6462011-10-20 16:36:43 -0700259 self.afe.create_job(
Chris Sosa5ca9d1b2012-07-30 14:41:18 -0700260 control_file=mox.And(mox.StrContains(self._BUILD),
261 mox.StrContains(self._URL % self._BUILD)),
Chris Masone8b7cd422012-02-22 13:16:11 -0800262 name=mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700263 control_type='Server',
Chris Masone5374c672012-03-05 15:11:39 -0800264 meta_hosts=[self._BOARD] * self._NUM,
Chris Masone99378582012-04-30 13:10:58 -0700265 dependencies=[],
266 priority='Low')
Chris Masone6fed6462011-10-20 16:36:43 -0700267 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700268 self.reimager._schedule_reimage_job(self._BUILD, self._BOARD, None,
269 self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700270
271
Chris Masone275ec902012-07-10 15:28:34 -0700272 def expect_attempt(self, canary_job, statuses, ex=None, check_hosts=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700273 """Sets up |self.reimager| to expect an attempt() that returns |success|
274
Chris Masone6ea0cad2012-07-02 09:43:36 -0700275 Also stubs out Reimager._clear_build_state(), should the caller wish
Chris Masoned368cc42012-03-07 15:16:59 -0800276 to set an expectation there as well.
277
Chris Masone275ec902012-07-10 15:28:34 -0700278 @param canary_job: a FakeJob representing the job we're expecting.
279 @param statuses: dict mapping a hostname to its job_status.Status.
280 Will be returned by job_status.gather_per_host_results
Chris Masone796fcf12012-02-22 16:53:31 -0800281 @param ex: if not None, |ex| is raised by get_jobs()
Chris Masone6fed6462011-10-20 16:36:43 -0700282 @return a FakeJob configured with appropriate expectations
283 """
Chris Masone6fed6462011-10-20 16:36:43 -0700284 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
Chris Masone6fed6462011-10-20 16:36:43 -0700285 self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
Chris Masone275ec902012-07-10 15:28:34 -0700286 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
287 self.mox.StubOutWithMock(self.reimager, '_clear_build_state')
288
Chris Masone517ef482012-07-23 15:36:36 -0700289 self.mox.StubOutWithMock(job_status, 'wait_for_jobs_to_start')
Chris Masone275ec902012-07-10 15:28:34 -0700290 self.mox.StubOutWithMock(job_status, 'wait_for_and_lock_job_hosts')
291 self.mox.StubOutWithMock(job_status, 'gather_job_hostnames')
Chris Masone517ef482012-07-23 15:36:36 -0700292 self.mox.StubOutWithMock(job_status, 'wait_for_jobs_to_finish')
Chris Masone275ec902012-07-10 15:28:34 -0700293 self.mox.StubOutWithMock(job_status, 'gather_per_host_results')
294 self.mox.StubOutWithMock(job_status, 'record_and_report_results')
295
Chris Sosa5ca9d1b2012-07-30 14:41:18 -0700296
Chris Masone275ec902012-07-10 15:28:34 -0700297 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone8b7cd422012-02-22 13:16:11 -0800298 self.reimager._schedule_reimage_job(self._BUILD,
Chris Masonec43448f2012-05-31 12:55:59 -0700299 self._BOARD,
300 None,
Chris Masone275ec902012-07-10 15:28:34 -0700301 self._NUM).AndReturn(canary_job)
Chris Masone62579122012-03-08 15:18:43 -0800302 if check_hosts:
Chris Masone62579122012-03-08 15:18:43 -0800303 self.reimager._count_usable_hosts(
304 mox.IgnoreArg()).AndReturn(self._NUM)
Chris Masone5374c672012-03-05 15:11:39 -0800305
Chris Masone517ef482012-07-23 15:36:36 -0700306 job_status.wait_for_jobs_to_start(self.afe, [canary_job])
Chris Masone275ec902012-07-10 15:28:34 -0700307 job_status.wait_for_and_lock_job_hosts(
Chris Masone517ef482012-07-23 15:36:36 -0700308 self.afe, [canary_job], self.manager).AndReturn(statuses.keys())
Chris Masone275ec902012-07-10 15:28:34 -0700309
310 if ex:
Chris Masone517ef482012-07-23 15:36:36 -0700311 job_status.wait_for_jobs_to_finish(self.afe,
312 [canary_job]).AndRaise(ex)
Chris Masone796fcf12012-02-22 16:53:31 -0800313 else:
Chris Masone517ef482012-07-23 15:36:36 -0700314 job_status.wait_for_jobs_to_finish(self.afe, [canary_job])
Chris Masone6ea0cad2012-07-02 09:43:36 -0700315 job_status.gather_per_host_results(
Chris Masone275ec902012-07-10 15:28:34 -0700316 mox.IgnoreArg(), mox.IgnoreArg(), [canary_job],
Chris Masone6ea0cad2012-07-02 09:43:36 -0700317 mox.StrContains(dynamic_suite.REIMAGE_JOB_NAME)).AndReturn(
318 statuses)
319
320 if statuses:
Chris Sosa5ca9d1b2012-07-30 14:41:18 -0700321 ret_val = reduce(lambda v,s: v and s.is_good(),
Chris Masone6ea0cad2012-07-02 09:43:36 -0700322 statuses.values(), True)
Chris Masone6ea0cad2012-07-02 09:43:36 -0700323 job_status.record_and_report_results(
324 statuses.values(), mox.IgnoreArg()).AndReturn(ret_val)
Chris Masone6fed6462011-10-20 16:36:43 -0700325
Chris Masone6fed6462011-10-20 16:36:43 -0700326
327 def testSuccessfulReimage(self):
328 """Should attempt a reimage and record success."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700329 canary = FakeJob()
Chris Masone517ef482012-07-23 15:36:36 -0700330 statuses = {canary.hostnames[0]: job_status.Status('GOOD',
331 canary.hostnames[0])}
Chris Masone6ea0cad2012-07-02 09:43:36 -0700332 self.expect_attempt(canary, statuses)
Chris Masone6fed6462011-10-20 16:36:43 -0700333
334 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone517ef482012-07-23 15:36:36 -0700335 self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
Chris Masone6fed6462011-10-20 16:36:43 -0700336 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700337 self.assertTrue(self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700338 rjob.record_entry, True,
339 self.manager))
Chris Masoned368cc42012-03-07 15:16:59 -0800340 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700341
342
343 def testFailedReimage(self):
344 """Should attempt a reimage and record failure."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700345 canary = FakeJob()
Chris Masone517ef482012-07-23 15:36:36 -0700346 statuses = {canary.hostnames[0]: job_status.Status('FAIL',
347 canary.hostnames[0])}
Chris Masone6ea0cad2012-07-02 09:43:36 -0700348 self.expect_attempt(canary, statuses)
Chris Masone6fed6462011-10-20 16:36:43 -0700349
350 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone517ef482012-07-23 15:36:36 -0700351 self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
Chris Masone6fed6462011-10-20 16:36:43 -0700352 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700353 self.assertFalse(self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700354 rjob.record_entry, True,
355 self.manager))
Chris Masoned368cc42012-03-07 15:16:59 -0800356 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700357
358
359 def testReimageThatNeverHappened(self):
360 """Should attempt a reimage and record that it didn't run."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700361 canary = FakeJob()
362 statuses = {'hostless': job_status.Status('ABORT', 'big_job_name')}
363 self.expect_attempt(canary, statuses)
Chris Masone6fed6462011-10-20 16:36:43 -0700364
365 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone6fed6462011-10-20 16:36:43 -0700366 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700367 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700368 rjob.record_entry, True, self.manager)
Chris Masoned368cc42012-03-07 15:16:59 -0800369 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700370
371
Chris Masone796fcf12012-02-22 16:53:31 -0800372 def testReimageThatRaised(self):
373 """Should attempt a reimage that raises an exception and record that."""
Chris Masone6ea0cad2012-07-02 09:43:36 -0700374 canary = FakeJob()
Chris Masone796fcf12012-02-22 16:53:31 -0800375 ex_message = 'Oh no!'
Chris Masone6ea0cad2012-07-02 09:43:36 -0700376 self.expect_attempt(canary, statuses={}, ex=Exception(ex_message))
Chris Masone796fcf12012-02-22 16:53:31 -0800377
378 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700379 rjob.record_entry(StatusContains.CreateFromStrings('START'))
380 rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
381 reason=ex_message))
382 rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
Chris Masone796fcf12012-02-22 16:53:31 -0800383 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 Masone62579122012-03-08 15:18:43 -0800386 self.reimager.clear_reimaged_host_state(self._BUILD)
387
388
389 def testSuccessfulReimageThatCouldNotScheduleRightAway(self):
Chris Masone99378582012-04-30 13:10:58 -0700390 """Should attempt reimage, ignoring host availability; record success.
Chris Masone62579122012-03-08 15:18:43 -0800391 """
Chris Masone6ea0cad2012-07-02 09:43:36 -0700392 canary = FakeJob()
Chris Masone517ef482012-07-23 15:36:36 -0700393 statuses = {canary.hostnames[0]: job_status.Status('GOOD',
394 canary.hostnames[0])}
Chris Masone6ea0cad2012-07-02 09:43:36 -0700395 self.expect_attempt(canary, statuses, check_hosts=False)
Chris Masone62579122012-03-08 15:18:43 -0800396
397 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone517ef482012-07-23 15:36:36 -0700398 self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
Chris Masone62579122012-03-08 15:18:43 -0800399 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700400 self.assertTrue(self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700401 rjob.record_entry, False,
402 self.manager))
Chris Masoned368cc42012-03-07 15:16:59 -0800403 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone796fcf12012-02-22 16:53:31 -0800404
405
Chris Masone5374c672012-03-05 15:11:39 -0800406 def testReimageThatCouldNotSchedule(self):
407 """Should attempt a reimage that can't be scheduled."""
Chris Masone502b71e2012-04-10 10:41:35 -0700408 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
409 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
410
411 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
412 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(1)
413
414 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700415 rjob.record_entry(StatusContains.CreateFromStrings('START'))
416 rjob.record_entry(
417 StatusContains.CreateFromStrings('WARN', reason='Too few hosts'))
418 rjob.record_entry(StatusContains.CreateFromStrings('END WARN'))
Chris Masone502b71e2012-04-10 10:41:35 -0700419 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700420 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700421 rjob.record_entry, True, self.manager)
Chris Masone502b71e2012-04-10 10:41:35 -0700422 self.reimager.clear_reimaged_host_state(self._BUILD)
423
424
425 def testReimageWithNoAvailableHosts(self):
426 """Should attempt a reimage while all hosts are dead."""
Chris Masone62579122012-03-08 15:18:43 -0800427 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
428 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone5374c672012-03-05 15:11:39 -0800429
430 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
431 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(0)
432
433 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700434 rjob.record_entry(StatusContains.CreateFromStrings('START'))
435 rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
436 reason='All hosts'))
437 rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
Chris Masone5374c672012-03-05 15:11:39 -0800438 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700439 self.reimager.attempt(self._BUILD, self._BOARD, None,
Chris Masone275ec902012-07-10 15:28:34 -0700440 rjob.record_entry, True, self.manager)
Chris Masoned368cc42012-03-07 15:16:59 -0800441 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone5374c672012-03-05 15:11:39 -0800442
443
Chris Masone6fed6462011-10-20 16:36:43 -0700444class SuiteTest(mox.MoxTestBase):
445 """Unit tests for dynamic_suite.Suite.
446
Chris Masone8b7cd422012-02-22 13:16:11 -0800447 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700448 @var _TAG: fake suite tag
449 """
450
Chris Masone8b7cd422012-02-22 13:16:11 -0800451 _BUILD = 'build'
452 _TAG = 'suite_tag'
Chris Masone6fed6462011-10-20 16:36:43 -0700453
454
455 def setUp(self):
Chris Masone6fed6462011-10-20 16:36:43 -0700456 super(SuiteTest, self).setUp()
457 self.afe = self.mox.CreateMock(frontend.AFE)
458 self.tko = self.mox.CreateMock(frontend.TKO)
459
460 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__)
461
Chris Masone275ec902012-07-10 15:28:34 -0700462 self.manager = self.mox.CreateMock(host_lock_manager.HostLockManager)
Chris Masone6fed6462011-10-20 16:36:43 -0700463 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
464
Chris Masone8d6e6412012-06-28 11:20:56 -0700465 self.files = {'one': FakeControlData(self._TAG, 'data_one', expr=True),
466 'two': FakeControlData(self._TAG, 'data_two'),
467 'three': FakeControlData(self._TAG, 'data_three')}
Chris Masone6fed6462011-10-20 16:36:43 -0700468
Chris Masone75a20612012-05-08 12:37:31 -0700469 self.files_to_filter = {
Chris Masone8d6e6412012-06-28 11:20:56 -0700470 'with/deps/...': FakeControlData(self._TAG, 'gets filtered'),
471 'with/profilers/...': FakeControlData(self._TAG, 'gets filtered')}
Chris Masone75a20612012-05-08 12:37:31 -0700472
Chris Masone6fed6462011-10-20 16:36:43 -0700473
474 def tearDown(self):
475 super(SuiteTest, self).tearDown()
476 shutil.rmtree(self.tmpdir, ignore_errors=True)
477
478
479 def expect_control_file_parsing(self):
480 """Expect an attempt to parse the 'control files' in |self.files|."""
Chris Masone75a20612012-05-08 12:37:31 -0700481 all_files = self.files.keys() + self.files_to_filter.keys()
Chris Masoneed356392012-05-08 14:07:13 -0700482 self._set_control_file_parsing_expectations(False, all_files,
483 self.files.iteritems())
484
485
486 def expect_control_file_reparsing(self):
487 """Expect re-parsing the 'control files' in |self.files|."""
488 all_files = self.files.keys() + self.files_to_filter.keys()
489 self._set_control_file_parsing_expectations(True, all_files,
490 self.files.iteritems())
491
492
493 def expect_racy_control_file_reparsing(self, new_files):
494 """Expect re-fetching and parsing of control files to return extra.
495
496 @param new_files: extra control files that showed up during scheduling.
497 """
498 all_files = (self.files.keys() + self.files_to_filter.keys() +
499 new_files.keys())
500 new_files.update(self.files)
501 self._set_control_file_parsing_expectations(True, all_files,
502 new_files.iteritems())
503
504
505 def _set_control_file_parsing_expectations(self, already_stubbed,
506 file_list, files_to_parse):
507 """Expect an attempt to parse the 'control files' in |files|.
508
509 @param already_stubbed: parse_control_string already stubbed out.
510 @param file_list: the files the dev server returns
511 @param files_to_parse: the {'name': FakeControlData} dict of files we
512 expect to get parsed.
513 """
514 if not already_stubbed:
515 self.mox.StubOutWithMock(control_data, 'parse_control_string')
516
517 self.getter.get_control_file_list().AndReturn(file_list)
518 for file, data in files_to_parse:
519 self.getter.get_control_file_contents(
520 file).InAnyOrder().AndReturn(data.string)
Chris Masone6fed6462011-10-20 16:36:43 -0700521 control_data.parse_control_string(
Chris Masoneed356392012-05-08 14:07:13 -0700522 data.string, raise_warnings=True).InAnyOrder().AndReturn(data)
Chris Masone6fed6462011-10-20 16:36:43 -0700523
524
525 def testFindAndParseStableTests(self):
526 """Should find only non-experimental tests that match a predicate."""
527 self.expect_control_file_parsing()
528 self.mox.ReplayAll()
529
530 predicate = lambda d: d.text == self.files['two'].string
531 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter, predicate)
532 self.assertEquals(len(tests), 1)
533 self.assertEquals(tests[0], self.files['two'])
534
535
536 def testFindAndParseTests(self):
537 """Should find all tests that match a predicate."""
538 self.expect_control_file_parsing()
539 self.mox.ReplayAll()
540
541 predicate = lambda d: d.text != self.files['two'].string
542 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter,
543 predicate,
544 add_experimental=True)
545 self.assertEquals(len(tests), 2)
546 self.assertTrue(self.files['one'] in tests)
547 self.assertTrue(self.files['three'] in tests)
548
549
550 def mock_control_file_parsing(self):
551 """Fake out find_and_parse_tests(), returning content from |self.files|.
552 """
553 for test in self.files.values():
554 test.text = test.string # mimic parsing.
555 self.mox.StubOutWithMock(dynamic_suite.Suite, 'find_and_parse_tests')
556 dynamic_suite.Suite.find_and_parse_tests(
557 mox.IgnoreArg(),
558 mox.IgnoreArg(),
559 add_experimental=True).AndReturn(self.files.values())
560
561
562 def testStableUnstableFilter(self):
563 """Should distinguish between experimental and stable tests."""
564 self.mock_control_file_parsing()
565 self.mox.ReplayAll()
566 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
Chris Masoned6f38c82012-02-22 14:53:42 -0800567 afe=self.afe, tko=self.tko)
Chris Masone6fed6462011-10-20 16:36:43 -0700568
569 self.assertTrue(self.files['one'] in suite.tests)
570 self.assertTrue(self.files['two'] in suite.tests)
571 self.assertTrue(self.files['one'] in suite.unstable_tests())
572 self.assertTrue(self.files['two'] in suite.stable_tests())
573 self.assertFalse(self.files['one'] in suite.stable_tests())
574 self.assertFalse(self.files['two'] in suite.unstable_tests())
575
576
577 def expect_job_scheduling(self, add_experimental):
578 """Expect jobs to be scheduled for 'tests' in |self.files|.
579
580 @param add_experimental: expect jobs for experimental tests as well.
581 """
582 for test in self.files.values():
583 if not add_experimental and test.experimental:
584 continue
585 self.afe.create_job(
586 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800587 name=mox.And(mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700588 mox.StrContains(test.name)),
589 control_type=mox.IgnoreArg(),
Chris Masone8b7cd422012-02-22 13:16:11 -0800590 meta_hosts=[dynamic_suite.VERSION_PREFIX + self._BUILD],
Chris Masonebafbbb02012-05-16 13:41:36 -0700591 dependencies=[],
592 keyvals={'build': self._BUILD, 'suite': self._TAG}
593 ).AndReturn(FakeJob())
Chris Masone6fed6462011-10-20 16:36:43 -0700594
595
596 def testScheduleTests(self):
597 """Should schedule stable and experimental tests with the AFE."""
598 self.mock_control_file_parsing()
599 self.expect_job_scheduling(add_experimental=True)
600
601 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800602 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800603 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800604 suite.schedule()
Chris Masone6fed6462011-10-20 16:36:43 -0700605
606
Scott Zawalski9ece6532012-02-28 14:10:47 -0500607 def testScheduleTestsAndRecord(self):
608 """Should schedule stable and experimental tests with the AFE."""
609 self.mock_control_file_parsing()
610 self.mox.ReplayAll()
611 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
612 afe=self.afe, tko=self.tko,
613 results_dir=self.tmpdir)
614 self.mox.ResetAll()
615 self.expect_job_scheduling(add_experimental=True)
616 self.mox.StubOutWithMock(suite, '_record_scheduled_jobs')
617 suite._record_scheduled_jobs()
618 self.mox.ReplayAll()
619 suite.schedule()
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500620 for job in suite._jobs:
621 self.assertTrue(hasattr(job, 'test_name'))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500622
623
Chris Masone6fed6462011-10-20 16:36:43 -0700624 def testScheduleStableTests(self):
625 """Should schedule only stable tests with the AFE."""
626 self.mock_control_file_parsing()
627 self.expect_job_scheduling(add_experimental=False)
628
629 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800630 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800631 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800632 suite.schedule(add_experimental=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700633
634
Chris Masonefef21382012-01-17 11:16:32 -0800635 def _createSuiteWithMockedTestsAndControlFiles(self):
636 """Create a Suite, using mocked tests and control file contents.
637
638 @return Suite object, after mocking out behavior needed to create it.
639 """
Chris Masonefef21382012-01-17 11:16:32 -0800640 self.expect_control_file_parsing()
641 self.mox.ReplayAll()
Scott Zawalski9ece6532012-02-28 14:10:47 -0500642 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800643 self.getter, self.afe,
644 self.tko)
Chris Masonefef21382012-01-17 11:16:32 -0800645 self.mox.ResetAll()
646 return suite
647
648
Chris Masoneed356392012-05-08 14:07:13 -0700649 def schedule_and_expect_these_results(self, suite, results, recorder):
650 self.mox.StubOutWithMock(suite, 'schedule')
651 suite.schedule(True)
Chris Masone275ec902012-07-10 15:28:34 -0700652 self.manager.unlock()
Chris Masone6fed6462011-10-20 16:36:43 -0700653 for result in results:
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500654 status = result[0]
Chris Masone99378582012-04-30 13:10:58 -0700655 test_name = result[1]
656 recorder.record_entry(
657 StatusContains.CreateFromStrings('START', test_name))
658 recorder.record_entry(
659 StatusContains.CreateFromStrings(*result)).InAnyOrder('results')
660 recorder.record_entry(
661 StatusContains.CreateFromStrings('END %s' % status, test_name))
Chris Masone8d6e6412012-06-28 11:20:56 -0700662 self.mox.StubOutWithMock(job_status, 'wait_for_results')
663 job_status.wait_for_results(self.afe, self.tko, suite._jobs).AndReturn(
664 map(lambda r: job_status.Status(*r), results))
Chris Masoneed356392012-05-08 14:07:13 -0700665
666
667 def testRunAndWaitSuccess(self):
668 """Should record successful results."""
669 suite = self._createSuiteWithMockedTestsAndControlFiles()
670
671 recorder = self.mox.CreateMock(base_job.base_job)
672 recorder.record_entry(
673 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
674
675 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
676 self.schedule_and_expect_these_results(suite, results, recorder)
677 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700678 self.mox.ReplayAll()
679
Chris Masone275ec902012-07-10 15:28:34 -0700680 suite.run_and_wait(recorder.record_entry, self.manager, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700681
682
683 def testRunAndWaitFailure(self):
684 """Should record failure to gather results."""
Chris Masonefef21382012-01-17 11:16:32 -0800685 suite = self._createSuiteWithMockedTestsAndControlFiles()
686
Chris Masone6fed6462011-10-20 16:36:43 -0700687 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700688 recorder.record_entry(
689 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
690 recorder.record_entry(
691 StatusContains.CreateFromStrings('FAIL', self._TAG, 'waiting'))
Chris Masone6fed6462011-10-20 16:36:43 -0700692
Chris Masone6fed6462011-10-20 16:36:43 -0700693 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone8b7cd422012-02-22 13:16:11 -0800694 suite.schedule(True)
Chris Masone275ec902012-07-10 15:28:34 -0700695 self.manager.unlock()
Chris Masone8d6e6412012-06-28 11:20:56 -0700696 self.mox.StubOutWithMock(job_status, 'wait_for_results')
697 job_status.wait_for_results(mox.IgnoreArg(),
698 mox.IgnoreArg(),
699 mox.IgnoreArg()).AndRaise(
700 Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700701 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700702 self.mox.ReplayAll()
703
Chris Masone275ec902012-07-10 15:28:34 -0700704 suite.run_and_wait(recorder.record_entry, self.manager, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700705
706
707 def testRunAndWaitScheduleFailure(self):
Chris Masonefef21382012-01-17 11:16:32 -0800708 """Should record failure to schedule jobs."""
709 suite = self._createSuiteWithMockedTestsAndControlFiles()
710
Chris Masone6fed6462011-10-20 16:36:43 -0700711 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700712 recorder.record_entry(
713 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
714 recorder.record_entry(
715 StatusContains.CreateFromStrings('FAIL', self._TAG, 'scheduling'))
Chris Masone6fed6462011-10-20 16:36:43 -0700716
Chris Masone6fed6462011-10-20 16:36:43 -0700717 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone99378582012-04-30 13:10:58 -0700718 suite.schedule(True).AndRaise(Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700719 self.expect_control_file_reparsing()
720 self.mox.ReplayAll()
721
Chris Masone275ec902012-07-10 15:28:34 -0700722 suite.run_and_wait(recorder.record_entry, self.manager, True)
Chris Masoneed356392012-05-08 14:07:13 -0700723
724
725 def testRunAndWaitDevServerRacyFailure(self):
726 """Should record discovery of dev server races in listing files."""
727 suite = self._createSuiteWithMockedTestsAndControlFiles()
728
729 recorder = self.mox.CreateMock(base_job.base_job)
730 recorder.record_entry(
731 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
732
733 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
734 self.schedule_and_expect_these_results(suite, results, recorder)
735
736 self.expect_racy_control_file_reparsing(
Chris Masone8d6e6412012-06-28 11:20:56 -0700737 {'new': FakeControlData(self._TAG, '!')})
Chris Masoneed356392012-05-08 14:07:13 -0700738
739 recorder.record_entry(
740 StatusContains.CreateFromStrings('FAIL', self._TAG, 'Dev Server'))
Chris Masone6fed6462011-10-20 16:36:43 -0700741 self.mox.ReplayAll()
742
Chris Masone275ec902012-07-10 15:28:34 -0700743 suite.run_and_wait(recorder.record_entry, self.manager, True)