blob: 487ad353944adbe4a56a4700f6eec424cd97210d [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 Masone8d6e6412012-06-28 11:20:56 -070021from autotest_lib.server.cros import job_status
22from autotest_lib.server.cros.dynamic_suite_fakes import FakeControlData
23from autotest_lib.server.cros.dynamic_suite_fakes import FakeHost, FakeJob
Chris Masonef63576d2012-06-29 11:13:31 -070024from autotest_lib.server.cros.dynamic_suite_fakes import FakeLabel, FakeResult
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 Masone62579122012-03-08 15:18:43 -080092 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 Masone2ef1d4e2011-12-20 11:06:53 -0800168 _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)
179 self.reimager = dynamic_suite.Reimager('', afe=self.afe, tko=self.tko)
Chris Masone5552dd72012-02-15 15:01:04 -0800180 self._CONFIG.override_config_value('CROS',
181 'sharding_factor',
182 "%d" % self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700183
184
185 def testEnsureVersionLabelAlreadyExists(self):
Chris Masone47c9e642012-04-25 14:22:18 -0700186 """Should tolerate a label that already exists."""
Chris Masone6fed6462011-10-20 16:36:43 -0700187 name = 'label'
Chris Masone47c9e642012-04-25 14:22:18 -0700188 error = proxy.ValidationError(
189 {'name': 'ValidationError',
190 'message': '{"name": "This value must be unique"}',
191 'traceback': ''},
192 'BAD')
193 self.afe.create_label(name=name).AndRaise(error)
Chris Masone6fed6462011-10-20 16:36:43 -0700194 self.mox.ReplayAll()
195 self.reimager._ensure_version_label(name)
196
197
198 def testEnsureVersionLabel(self):
199 """Should create a label if it doesn't already exist."""
200 name = 'label'
Chris Masone6fed6462011-10-20 16:36:43 -0700201 self.afe.create_label(name=name)
202 self.mox.ReplayAll()
203 self.reimager._ensure_version_label(name)
204
205
Chris Masone5374c672012-03-05 15:11:39 -0800206 def testCountHostsByBoardAndPool(self):
207 """Should count available hosts by board and pool."""
208 spec = [self._BOARD, 'pool:bvt']
209 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()])
210 self.mox.ReplayAll()
211 self.assertEquals(self.reimager._count_usable_hosts(spec), 1)
212
213
214 def testCountHostsByBoard(self):
215 """Should count available hosts by board."""
216 spec = [self._BOARD]
217 self.afe.get_hosts(multiple_labels=spec).AndReturn([FakeHost()] * 2)
218 self.mox.ReplayAll()
219 self.assertEquals(self.reimager._count_usable_hosts(spec), 2)
220
221
222 def testCountZeroHostsByBoard(self):
223 """Should count the available hosts, by board, getting zero."""
224 spec = [self._BOARD]
225 self.afe.get_hosts(multiple_labels=spec).AndReturn([])
226 self.mox.ReplayAll()
227 self.assertEquals(self.reimager._count_usable_hosts(spec), 0)
228
229
Chris Masone6fed6462011-10-20 16:36:43 -0700230 def testInjectVars(self):
231 """Should inject dict of varibles into provided strings."""
232 def find_all_in(d, s):
233 """Returns true if all key-value pairs in |d| are printed in |s|."""
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800234 for k,v in d.iteritems():
235 if isinstance(v, str):
236 if "%s='%s'\n" % (k,v) not in s:
237 return False
238 else:
239 if "%s=%r\n" % (k,v) not in s:
240 return False
241 return True
Chris Masone6fed6462011-10-20 16:36:43 -0700242
Chris Masone6cb0d0d2012-03-05 15:37:49 -0800243 v = {'v1': 'one', 'v2': 'two', 'v3': None, 'v4': False, 'v5': 5}
Chris Masone8b764252012-01-17 11:12:51 -0800244 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, '')))
245 self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, 'ctrl')))
Chris Masone6fed6462011-10-20 16:36:43 -0700246
247
248 def testReportResultsGood(self):
249 """Should report results in the case where all jobs passed."""
Chris Masonef63576d2012-06-29 11:13:31 -0700250 H1 = 'host1'
251
252 job = FakeJob()
Chris Masone6fed6462011-10-20 16:36:43 -0700253 job.result = True
Chris Masonef63576d2012-06-29 11:13:31 -0700254 job.results_platform_map = {'netbook': {'Completed' : [H1]}}
255 job.test_status = {H1: frontend.TestResults()}
256 job.test_status[H1].good = [FakeResult('a')]
257
Chris Masone6fed6462011-10-20 16:36:43 -0700258 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700259 recorder.record_entry(StatusContains.CreateFromStrings('START'))
260 recorder.record_entry(StatusContains.CreateFromStrings('GOOD', H1))
261 recorder.record_entry(StatusContains.CreateFromStrings('END GOOD'))
Chris Masone6fed6462011-10-20 16:36:43 -0700262 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700263 self.reimager._report_results(job, recorder.record_entry)
Chris Masone6fed6462011-10-20 16:36:43 -0700264
265
266 def testReportResultsBad(self):
267 """Should report results in various job failure cases.
268
269 In this test scenario, there are five hosts, all the 'netbook' platform.
270
271 h1: Did not run
272 h2: Two failed tests
273 h3: Two aborted tests
274 h4: completed, GOOD
275 h5: completed, GOOD
276 """
277 H1 = 'host1'
278 H2 = 'host2'
279 H3 = 'host3'
280 H4 = 'host4'
281 H5 = 'host5'
282
Chris Masone6fed6462011-10-20 16:36:43 -0700283
284 # The RPC-client-side Job object that is annotated with results.
285 job = FakeJob()
286 job.result = None # job failed, there are results to report.
287
288 # The semantics of |results_platform_map| and |test_results| are
289 # drawn from frontend.AFE.poll_all_jobs()
290 job.results_platform_map = {'netbook': {'Aborted' : [H3],
Chris Masone8b7cd422012-02-22 13:16:11 -0800291 'Completed' : [H1, H4, H5],
292 'Failed': [H2]
Chris Masone6fed6462011-10-20 16:36:43 -0700293 }
294 }
295 # Gin up fake results for H2 and H3 failure cases.
296 h2 = frontend.TestResults()
297 h2.fail = [FakeResult('a'), FakeResult('b')]
298 h3 = frontend.TestResults()
299 h3.fail = [FakeResult('a'), FakeResult('b')]
Chris Masonef63576d2012-06-29 11:13:31 -0700300 h4 = frontend.TestResults()
301 h4.good = [FakeResult('c')]
302 h5 = frontend.TestResults()
303 h5.good = [FakeResult('c')]
Chris Masone6fed6462011-10-20 16:36:43 -0700304 # Skipping H1 in |test_status| dict means that it did not get run.
Chris Masonef63576d2012-06-29 11:13:31 -0700305 job.test_status = {H2: h2, H3: h3, H4: h4, H5: h5}
Chris Masone6fed6462011-10-20 16:36:43 -0700306
307 # Set up recording expectations.
308 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700309 def set_recording_expectations(code, hostname, reason):
310 rjob.record_entry(
311 StatusContains.CreateFromStrings('START')).InAnyOrder()
312 rjob.record_entry(
313 StatusContains.CreateFromStrings(code,
314 hostname,
315 reason)).InAnyOrder()
316 rjob.record_entry(
317 StatusContains.CreateFromStrings('END %s' % code)).InAnyOrder()
318
Chris Masone6fed6462011-10-20 16:36:43 -0700319 for res in h2.fail:
Chris Masonef63576d2012-06-29 11:13:31 -0700320 set_recording_expectations('FAIL', H2, res.reason)
Chris Masone6fed6462011-10-20 16:36:43 -0700321 for res in h3.fail:
Chris Masonef63576d2012-06-29 11:13:31 -0700322 set_recording_expectations('ABORT', H3, res.reason)
323
324 set_recording_expectations('GOOD', H4, None)
325 set_recording_expectations('GOOD', H5, None)
326 set_recording_expectations('ERROR', H1, None)
Chris Masone6fed6462011-10-20 16:36:43 -0700327
328 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700329 self.reimager._report_results(job, rjob.record_entry)
Chris Masone6fed6462011-10-20 16:36:43 -0700330
331
332 def testScheduleJob(self):
333 """Should be able to create a job with the AFE."""
334 # Fake out getting the autoupdate control file contents.
335 cf_getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
336 cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
337 self.reimager._cf_getter = cf_getter
338
Chris Masone6dca10a2012-02-15 15:41:42 -0800339 self._CONFIG.override_config_value('CROS',
340 'image_url_pattern',
341 self._URL)
Chris Masone6fed6462011-10-20 16:36:43 -0700342 self.afe.create_job(
Chris Masone8b7cd422012-02-22 13:16:11 -0800343 control_file=mox.And(mox.StrContains(self._BUILD),
344 mox.StrContains(self._URL % self._BUILD)),
345 name=mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700346 control_type='Server',
Chris Masone5374c672012-03-05 15:11:39 -0800347 meta_hosts=[self._BOARD] * self._NUM,
Chris Masone99378582012-04-30 13:10:58 -0700348 dependencies=[],
349 priority='Low')
Chris Masone6fed6462011-10-20 16:36:43 -0700350 self.mox.ReplayAll()
Chris Masonec43448f2012-05-31 12:55:59 -0700351 self.reimager._schedule_reimage_job(self._BUILD, self._BOARD, None,
352 self._NUM)
Chris Masone6fed6462011-10-20 16:36:43 -0700353
354
Chris Masone62579122012-03-08 15:18:43 -0800355 def expect_attempt(self, success, ex=None, check_hosts=True):
Chris Masone6fed6462011-10-20 16:36:43 -0700356 """Sets up |self.reimager| to expect an attempt() that returns |success|
357
Chris Masoned368cc42012-03-07 15:16:59 -0800358 Also stubs out Reimger._clear_build_state(), should the caller wish
359 to set an expectation there as well.
360
Chris Masone796fcf12012-02-22 16:53:31 -0800361 @param success: the value returned by poll_job_results()
362 @param ex: if not None, |ex| is raised by get_jobs()
Chris Masone6fed6462011-10-20 16:36:43 -0700363 @return a FakeJob configured with appropriate expectations
364 """
365 canary = FakeJob()
366 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
Chris Masone8b7cd422012-02-22 13:16:11 -0800367 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone6fed6462011-10-20 16:36:43 -0700368
369 self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
Chris Masone8b7cd422012-02-22 13:16:11 -0800370 self.reimager._schedule_reimage_job(self._BUILD,
Chris Masonec43448f2012-05-31 12:55:59 -0700371 self._BOARD,
372 None,
373 self._NUM).AndReturn(canary)
Chris Masone62579122012-03-08 15:18:43 -0800374 if check_hosts:
375 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
376 self.reimager._count_usable_hosts(
377 mox.IgnoreArg()).AndReturn(self._NUM)
Chris Masone5374c672012-03-05 15:11:39 -0800378
Chris Masone6fed6462011-10-20 16:36:43 -0700379 if success is not None:
380 self.mox.StubOutWithMock(self.reimager, '_report_results')
381 self.reimager._report_results(canary, mox.IgnoreArg())
Chris Masoned368cc42012-03-07 15:16:59 -0800382 canary.results_platform_map = {None: {'Total': [canary.hostname]}}
383
Chris Masone6fed6462011-10-20 16:36:43 -0700384
385 self.afe.get_jobs(id=canary.id, not_yet_run=True).AndReturn([])
Chris Masone796fcf12012-02-22 16:53:31 -0800386 if ex is not None:
387 self.afe.get_jobs(id=canary.id, finished=True).AndRaise(ex)
388 else:
389 self.afe.get_jobs(id=canary.id, finished=True).AndReturn([canary])
390 self.afe.poll_job_results(mox.IgnoreArg(),
391 canary, 0).AndReturn(success)
Chris Masone6fed6462011-10-20 16:36:43 -0700392
Chris Masoned368cc42012-03-07 15:16:59 -0800393 self.mox.StubOutWithMock(self.reimager, '_clear_build_state')
394
Chris Masone6fed6462011-10-20 16:36:43 -0700395 return canary
396
397
398 def testSuccessfulReimage(self):
399 """Should attempt a reimage and record success."""
Chris Masone62579122012-03-08 15:18:43 -0800400 canary = self.expect_attempt(success=True)
Chris Masone6fed6462011-10-20 16:36:43 -0700401
402 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masoned368cc42012-03-07 15:16:59 -0800403 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
Chris Masone6fed6462011-10-20 16:36:43 -0700404 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700405 self.assertTrue(self.reimager.attempt(self._BUILD, self._BOARD, None,
406 rjob.record_entry, True))
Chris Masoned368cc42012-03-07 15:16:59 -0800407 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700408
409
410 def testFailedReimage(self):
411 """Should attempt a reimage and record failure."""
Chris Masone62579122012-03-08 15:18:43 -0800412 canary = self.expect_attempt(success=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700413
414 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masoned368cc42012-03-07 15:16:59 -0800415 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
Chris Masone6fed6462011-10-20 16:36:43 -0700416 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700417 self.assertFalse(self.reimager.attempt(self._BUILD, self._BOARD, None,
418 rjob.record_entry, True))
Chris Masoned368cc42012-03-07 15:16:59 -0800419 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700420
421
422 def testReimageThatNeverHappened(self):
423 """Should attempt a reimage and record that it didn't run."""
Chris Masone62579122012-03-08 15:18:43 -0800424 canary = self.expect_attempt(success=None)
Chris Masone6fed6462011-10-20 16:36:43 -0700425
426 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700427 rjob.record_entry(StatusContains.CreateFromStrings('START'))
428 rjob.record_entry(StatusContains.CreateFromStrings('FAIL'))
429 rjob.record_entry(StatusContains.CreateFromStrings('END FAIL'))
Chris Masone6fed6462011-10-20 16:36:43 -0700430 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700431 self.reimager.attempt(self._BUILD, self._BOARD, None,
432 rjob.record_entry, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800433 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone6fed6462011-10-20 16:36:43 -0700434
435
Chris Masone796fcf12012-02-22 16:53:31 -0800436 def testReimageThatRaised(self):
437 """Should attempt a reimage that raises an exception and record that."""
438 ex_message = 'Oh no!'
Chris Masone62579122012-03-08 15:18:43 -0800439 canary = self.expect_attempt(success=None, ex=Exception(ex_message))
Chris Masone796fcf12012-02-22 16:53:31 -0800440
441 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700442 rjob.record_entry(StatusContains.CreateFromStrings('START'))
443 rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
444 reason=ex_message))
445 rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
Chris Masone796fcf12012-02-22 16:53:31 -0800446 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700447 self.reimager.attempt(self._BUILD, self._BOARD, None,
448 rjob.record_entry, True)
Chris Masone62579122012-03-08 15:18:43 -0800449 self.reimager.clear_reimaged_host_state(self._BUILD)
450
451
452 def testSuccessfulReimageThatCouldNotScheduleRightAway(self):
Chris Masone99378582012-04-30 13:10:58 -0700453 """Should attempt reimage, ignoring host availability; record success.
Chris Masone62579122012-03-08 15:18:43 -0800454 """
455 canary = self.expect_attempt(success=True, check_hosts=False)
456
457 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masone62579122012-03-08 15:18:43 -0800458 self.reimager._clear_build_state(mox.StrContains(canary.hostname))
459 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700460 self.assertTrue(self.reimager.attempt(self._BUILD, self._BOARD, None,
461 rjob.record_entry, False))
Chris Masoned368cc42012-03-07 15:16:59 -0800462 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone796fcf12012-02-22 16:53:31 -0800463
464
Chris Masone5374c672012-03-05 15:11:39 -0800465 def testReimageThatCouldNotSchedule(self):
466 """Should attempt a reimage that can't be scheduled."""
Chris Masone502b71e2012-04-10 10:41:35 -0700467 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
468 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
469
470 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
471 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(1)
472
473 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700474 rjob.record_entry(StatusContains.CreateFromStrings('START'))
475 rjob.record_entry(
476 StatusContains.CreateFromStrings('WARN', reason='Too few hosts'))
477 rjob.record_entry(StatusContains.CreateFromStrings('END WARN'))
Chris Masone502b71e2012-04-10 10:41:35 -0700478 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700479 self.reimager.attempt(self._BUILD, self._BOARD, None,
480 rjob.record_entry, True)
Chris Masone502b71e2012-04-10 10:41:35 -0700481 self.reimager.clear_reimaged_host_state(self._BUILD)
482
483
484 def testReimageWithNoAvailableHosts(self):
485 """Should attempt a reimage while all hosts are dead."""
Chris Masone62579122012-03-08 15:18:43 -0800486 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
487 self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
Chris Masone5374c672012-03-05 15:11:39 -0800488
489 self.mox.StubOutWithMock(self.reimager, '_count_usable_hosts')
490 self.reimager._count_usable_hosts(mox.IgnoreArg()).AndReturn(0)
491
492 rjob = self.mox.CreateMock(base_job.base_job)
Chris Masonef63576d2012-06-29 11:13:31 -0700493 rjob.record_entry(StatusContains.CreateFromStrings('START'))
494 rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
495 reason='All hosts'))
496 rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
Chris Masone5374c672012-03-05 15:11:39 -0800497 self.mox.ReplayAll()
Chris Masonef63576d2012-06-29 11:13:31 -0700498 self.reimager.attempt(self._BUILD, self._BOARD, None,
499 rjob.record_entry, True)
Chris Masoned368cc42012-03-07 15:16:59 -0800500 self.reimager.clear_reimaged_host_state(self._BUILD)
Chris Masone5374c672012-03-05 15:11:39 -0800501
502
Chris Masone6fed6462011-10-20 16:36:43 -0700503class SuiteTest(mox.MoxTestBase):
504 """Unit tests for dynamic_suite.Suite.
505
Chris Masone8b7cd422012-02-22 13:16:11 -0800506 @var _BUILD: fake build
Chris Masone6fed6462011-10-20 16:36:43 -0700507 @var _TAG: fake suite tag
508 """
509
Chris Masone8b7cd422012-02-22 13:16:11 -0800510 _BUILD = 'build'
511 _TAG = 'suite_tag'
Chris Masone6fed6462011-10-20 16:36:43 -0700512
513
514 def setUp(self):
Chris Masone6fed6462011-10-20 16:36:43 -0700515 super(SuiteTest, self).setUp()
516 self.afe = self.mox.CreateMock(frontend.AFE)
517 self.tko = self.mox.CreateMock(frontend.TKO)
518
519 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__)
520
521 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
522
Chris Masone8d6e6412012-06-28 11:20:56 -0700523 self.files = {'one': FakeControlData(self._TAG, 'data_one', expr=True),
524 'two': FakeControlData(self._TAG, 'data_two'),
525 'three': FakeControlData(self._TAG, 'data_three')}
Chris Masone6fed6462011-10-20 16:36:43 -0700526
Chris Masone75a20612012-05-08 12:37:31 -0700527 self.files_to_filter = {
Chris Masone8d6e6412012-06-28 11:20:56 -0700528 'with/deps/...': FakeControlData(self._TAG, 'gets filtered'),
529 'with/profilers/...': FakeControlData(self._TAG, 'gets filtered')}
Chris Masone75a20612012-05-08 12:37:31 -0700530
Chris Masone6fed6462011-10-20 16:36:43 -0700531
532 def tearDown(self):
533 super(SuiteTest, self).tearDown()
534 shutil.rmtree(self.tmpdir, ignore_errors=True)
535
536
537 def expect_control_file_parsing(self):
538 """Expect an attempt to parse the 'control files' in |self.files|."""
Chris Masone75a20612012-05-08 12:37:31 -0700539 all_files = self.files.keys() + self.files_to_filter.keys()
Chris Masoneed356392012-05-08 14:07:13 -0700540 self._set_control_file_parsing_expectations(False, all_files,
541 self.files.iteritems())
542
543
544 def expect_control_file_reparsing(self):
545 """Expect re-parsing the 'control files' in |self.files|."""
546 all_files = self.files.keys() + self.files_to_filter.keys()
547 self._set_control_file_parsing_expectations(True, all_files,
548 self.files.iteritems())
549
550
551 def expect_racy_control_file_reparsing(self, new_files):
552 """Expect re-fetching and parsing of control files to return extra.
553
554 @param new_files: extra control files that showed up during scheduling.
555 """
556 all_files = (self.files.keys() + self.files_to_filter.keys() +
557 new_files.keys())
558 new_files.update(self.files)
559 self._set_control_file_parsing_expectations(True, all_files,
560 new_files.iteritems())
561
562
563 def _set_control_file_parsing_expectations(self, already_stubbed,
564 file_list, files_to_parse):
565 """Expect an attempt to parse the 'control files' in |files|.
566
567 @param already_stubbed: parse_control_string already stubbed out.
568 @param file_list: the files the dev server returns
569 @param files_to_parse: the {'name': FakeControlData} dict of files we
570 expect to get parsed.
571 """
572 if not already_stubbed:
573 self.mox.StubOutWithMock(control_data, 'parse_control_string')
574
575 self.getter.get_control_file_list().AndReturn(file_list)
576 for file, data in files_to_parse:
577 self.getter.get_control_file_contents(
578 file).InAnyOrder().AndReturn(data.string)
Chris Masone6fed6462011-10-20 16:36:43 -0700579 control_data.parse_control_string(
Chris Masoneed356392012-05-08 14:07:13 -0700580 data.string, raise_warnings=True).InAnyOrder().AndReturn(data)
Chris Masone6fed6462011-10-20 16:36:43 -0700581
582
583 def testFindAndParseStableTests(self):
584 """Should find only non-experimental tests that match a predicate."""
585 self.expect_control_file_parsing()
586 self.mox.ReplayAll()
587
588 predicate = lambda d: d.text == self.files['two'].string
589 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter, predicate)
590 self.assertEquals(len(tests), 1)
591 self.assertEquals(tests[0], self.files['two'])
592
593
594 def testFindAndParseTests(self):
595 """Should find all tests that match a predicate."""
596 self.expect_control_file_parsing()
597 self.mox.ReplayAll()
598
599 predicate = lambda d: d.text != self.files['two'].string
600 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter,
601 predicate,
602 add_experimental=True)
603 self.assertEquals(len(tests), 2)
604 self.assertTrue(self.files['one'] in tests)
605 self.assertTrue(self.files['three'] in tests)
606
607
608 def mock_control_file_parsing(self):
609 """Fake out find_and_parse_tests(), returning content from |self.files|.
610 """
611 for test in self.files.values():
612 test.text = test.string # mimic parsing.
613 self.mox.StubOutWithMock(dynamic_suite.Suite, 'find_and_parse_tests')
614 dynamic_suite.Suite.find_and_parse_tests(
615 mox.IgnoreArg(),
616 mox.IgnoreArg(),
617 add_experimental=True).AndReturn(self.files.values())
618
619
620 def testStableUnstableFilter(self):
621 """Should distinguish between experimental and stable tests."""
622 self.mock_control_file_parsing()
623 self.mox.ReplayAll()
624 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
Chris Masoned6f38c82012-02-22 14:53:42 -0800625 afe=self.afe, tko=self.tko)
Chris Masone6fed6462011-10-20 16:36:43 -0700626
627 self.assertTrue(self.files['one'] in suite.tests)
628 self.assertTrue(self.files['two'] in suite.tests)
629 self.assertTrue(self.files['one'] in suite.unstable_tests())
630 self.assertTrue(self.files['two'] in suite.stable_tests())
631 self.assertFalse(self.files['one'] in suite.stable_tests())
632 self.assertFalse(self.files['two'] in suite.unstable_tests())
633
634
635 def expect_job_scheduling(self, add_experimental):
636 """Expect jobs to be scheduled for 'tests' in |self.files|.
637
638 @param add_experimental: expect jobs for experimental tests as well.
639 """
640 for test in self.files.values():
641 if not add_experimental and test.experimental:
642 continue
643 self.afe.create_job(
644 control_file=test.text,
Chris Masone8b7cd422012-02-22 13:16:11 -0800645 name=mox.And(mox.StrContains(self._BUILD),
Chris Masone6fed6462011-10-20 16:36:43 -0700646 mox.StrContains(test.name)),
647 control_type=mox.IgnoreArg(),
Chris Masone8b7cd422012-02-22 13:16:11 -0800648 meta_hosts=[dynamic_suite.VERSION_PREFIX + self._BUILD],
Chris Masonebafbbb02012-05-16 13:41:36 -0700649 dependencies=[],
650 keyvals={'build': self._BUILD, 'suite': self._TAG}
651 ).AndReturn(FakeJob())
Chris Masone6fed6462011-10-20 16:36:43 -0700652
653
654 def testScheduleTests(self):
655 """Should schedule stable and experimental tests with the AFE."""
656 self.mock_control_file_parsing()
657 self.expect_job_scheduling(add_experimental=True)
658
659 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800660 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800661 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800662 suite.schedule()
Chris Masone6fed6462011-10-20 16:36:43 -0700663
664
Scott Zawalski9ece6532012-02-28 14:10:47 -0500665 def testScheduleTestsAndRecord(self):
666 """Should schedule stable and experimental tests with the AFE."""
667 self.mock_control_file_parsing()
668 self.mox.ReplayAll()
669 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
670 afe=self.afe, tko=self.tko,
671 results_dir=self.tmpdir)
672 self.mox.ResetAll()
673 self.expect_job_scheduling(add_experimental=True)
674 self.mox.StubOutWithMock(suite, '_record_scheduled_jobs')
675 suite._record_scheduled_jobs()
676 self.mox.ReplayAll()
677 suite.schedule()
Scott Zawalskie5bb1c52012-02-29 13:15:50 -0500678 for job in suite._jobs:
679 self.assertTrue(hasattr(job, 'test_name'))
Scott Zawalski9ece6532012-02-28 14:10:47 -0500680
681
Chris Masone6fed6462011-10-20 16:36:43 -0700682 def testScheduleStableTests(self):
683 """Should schedule only stable tests with the AFE."""
684 self.mock_control_file_parsing()
685 self.expect_job_scheduling(add_experimental=False)
686
687 self.mox.ReplayAll()
Chris Masone8b7cd422012-02-22 13:16:11 -0800688 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800689 afe=self.afe, tko=self.tko)
Chris Masone8b7cd422012-02-22 13:16:11 -0800690 suite.schedule(add_experimental=False)
Chris Masone6fed6462011-10-20 16:36:43 -0700691
692
Chris Masonefef21382012-01-17 11:16:32 -0800693 def _createSuiteWithMockedTestsAndControlFiles(self):
694 """Create a Suite, using mocked tests and control file contents.
695
696 @return Suite object, after mocking out behavior needed to create it.
697 """
Chris Masonefef21382012-01-17 11:16:32 -0800698 self.expect_control_file_parsing()
699 self.mox.ReplayAll()
Scott Zawalski9ece6532012-02-28 14:10:47 -0500700 suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
Chris Masoned6f38c82012-02-22 14:53:42 -0800701 self.getter, self.afe,
702 self.tko)
Chris Masonefef21382012-01-17 11:16:32 -0800703 self.mox.ResetAll()
704 return suite
705
706
Chris Masoneed356392012-05-08 14:07:13 -0700707 def schedule_and_expect_these_results(self, suite, results, recorder):
708 self.mox.StubOutWithMock(suite, 'schedule')
709 suite.schedule(True)
Chris Masone6fed6462011-10-20 16:36:43 -0700710 for result in results:
Scott Zawalskiab25bd62012-02-10 18:29:12 -0500711 status = result[0]
Chris Masone99378582012-04-30 13:10:58 -0700712 test_name = result[1]
713 recorder.record_entry(
714 StatusContains.CreateFromStrings('START', test_name))
715 recorder.record_entry(
716 StatusContains.CreateFromStrings(*result)).InAnyOrder('results')
717 recorder.record_entry(
718 StatusContains.CreateFromStrings('END %s' % status, test_name))
Chris Masone8d6e6412012-06-28 11:20:56 -0700719 self.mox.StubOutWithMock(job_status, 'wait_for_results')
720 job_status.wait_for_results(self.afe, self.tko, suite._jobs).AndReturn(
721 map(lambda r: job_status.Status(*r), results))
Chris Masoneed356392012-05-08 14:07:13 -0700722
723
724 def testRunAndWaitSuccess(self):
725 """Should record successful results."""
726 suite = self._createSuiteWithMockedTestsAndControlFiles()
727
728 recorder = self.mox.CreateMock(base_job.base_job)
729 recorder.record_entry(
730 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
731
732 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
733 self.schedule_and_expect_these_results(suite, results, recorder)
734 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700735 self.mox.ReplayAll()
736
Chris Masone99378582012-04-30 13:10:58 -0700737 suite.run_and_wait(recorder.record_entry, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700738
739
740 def testRunAndWaitFailure(self):
741 """Should record failure to gather results."""
Chris Masonefef21382012-01-17 11:16:32 -0800742 suite = self._createSuiteWithMockedTestsAndControlFiles()
743
Chris Masone6fed6462011-10-20 16:36:43 -0700744 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700745 recorder.record_entry(
746 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
747 recorder.record_entry(
748 StatusContains.CreateFromStrings('FAIL', self._TAG, 'waiting'))
Chris Masone6fed6462011-10-20 16:36:43 -0700749
Chris Masone6fed6462011-10-20 16:36:43 -0700750 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone8b7cd422012-02-22 13:16:11 -0800751 suite.schedule(True)
Chris Masone8d6e6412012-06-28 11:20:56 -0700752 self.mox.StubOutWithMock(job_status, 'wait_for_results')
753 job_status.wait_for_results(mox.IgnoreArg(),
754 mox.IgnoreArg(),
755 mox.IgnoreArg()).AndRaise(
756 Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700757 self.expect_control_file_reparsing()
Chris Masone6fed6462011-10-20 16:36:43 -0700758 self.mox.ReplayAll()
759
Chris Masone99378582012-04-30 13:10:58 -0700760 suite.run_and_wait(recorder.record_entry, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700761
762
763 def testRunAndWaitScheduleFailure(self):
Chris Masonefef21382012-01-17 11:16:32 -0800764 """Should record failure to schedule jobs."""
765 suite = self._createSuiteWithMockedTestsAndControlFiles()
766
Chris Masone6fed6462011-10-20 16:36:43 -0700767 recorder = self.mox.CreateMock(base_job.base_job)
Chris Masone99378582012-04-30 13:10:58 -0700768 recorder.record_entry(
769 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
770 recorder.record_entry(
771 StatusContains.CreateFromStrings('FAIL', self._TAG, 'scheduling'))
Chris Masone6fed6462011-10-20 16:36:43 -0700772
Chris Masone6fed6462011-10-20 16:36:43 -0700773 self.mox.StubOutWithMock(suite, 'schedule')
Chris Masone99378582012-04-30 13:10:58 -0700774 suite.schedule(True).AndRaise(Exception('Expected during test.'))
Chris Masoneed356392012-05-08 14:07:13 -0700775 self.expect_control_file_reparsing()
776 self.mox.ReplayAll()
777
778 suite.run_and_wait(recorder.record_entry, True)
779
780
781 def testRunAndWaitDevServerRacyFailure(self):
782 """Should record discovery of dev server races in listing files."""
783 suite = self._createSuiteWithMockedTestsAndControlFiles()
784
785 recorder = self.mox.CreateMock(base_job.base_job)
786 recorder.record_entry(
787 StatusContains.CreateFromStrings('INFO', 'Start %s' % self._TAG))
788
789 results = [('GOOD', 'good'), ('FAIL', 'bad', 'reason')]
790 self.schedule_and_expect_these_results(suite, results, recorder)
791
792 self.expect_racy_control_file_reparsing(
Chris Masone8d6e6412012-06-28 11:20:56 -0700793 {'new': FakeControlData(self._TAG, '!')})
Chris Masoneed356392012-05-08 14:07:13 -0700794
795 recorder.record_entry(
796 StatusContains.CreateFromStrings('FAIL', self._TAG, 'Dev Server'))
Chris Masone6fed6462011-10-20 16:36:43 -0700797 self.mox.ReplayAll()
798
Chris Masone99378582012-04-30 13:10:58 -0700799 suite.run_and_wait(recorder.record_entry, True)
Chris Masone6fed6462011-10-20 16:36:43 -0700800
801
802if __name__ == '__main__':
803 unittest.main()