blob: 696ce31a40d128f35ac1f1ce4ecbdf5fffae0a96 [file] [log] [blame]
Chris Masone6fed6462011-10-20 16:36:43 -07001#!/usr/bin/python
2#
3# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
4# 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
11import shutil
12import tempfile
13import time
14import unittest
15
16from autotest_lib.client.common_lib import base_job, control_data
17from autotest_lib.server.cros import control_file_getter, dynamic_suite
18from autotest_lib.server import frontend
19
20class FakeJob(object):
21 """Faked out RPC-client-side Job object."""
22 def __init__(self, id=0, statuses=[]):
23 self.id = id
24 self.name = 'Fake Job %d' % self.id
25 self.statuses = statuses
26
27
28class ReimagerTest(mox.MoxTestBase):
29 """Unit tests for dynamic_suite.Reimager.
30
31 @var _URL: fake image url
32 @var _NAME: fake image name
33 @var _NUM: fake number of machines to run on
34 @var _BOARD: fake board to reimage
35 """
36
Chris Masone2ef1d4e2011-12-20 11:06:53 -080037 _URL = 'http://nothing/%s'
Chris Masone6fed6462011-10-20 16:36:43 -070038 _NAME = 'name'
39 _NUM = 4
40 _BOARD = 'board'
41
42
43 def setUp(self):
44 super(ReimagerTest, self).setUp()
45 self.afe = self.mox.CreateMock(frontend.AFE)
46 self.tko = self.mox.CreateMock(frontend.TKO)
47 self.reimager = dynamic_suite.Reimager('', afe=self.afe, tko=self.tko)
48
49
50 def testEnsureVersionLabelAlreadyExists(self):
51 """Should not create a label if it already exists."""
52 name = 'label'
53 self.afe.get_labels(name=name).AndReturn([name])
54 self.mox.ReplayAll()
55 self.reimager._ensure_version_label(name)
56
57
58 def testEnsureVersionLabel(self):
59 """Should create a label if it doesn't already exist."""
60 name = 'label'
61 self.afe.get_labels(name=name).AndReturn([])
62 self.afe.create_label(name=name)
63 self.mox.ReplayAll()
64 self.reimager._ensure_version_label(name)
65
66
67 def testInjectVars(self):
68 """Should inject dict of varibles into provided strings."""
69 def find_all_in(d, s):
70 """Returns true if all key-value pairs in |d| are printed in |s|."""
71 return reduce(lambda b,i: "%s='%s'\n" % i in s, d.iteritems(), True)
72
73 v = {'v1': 'one', 'v2': 'two'}
74 self.assertTrue(find_all_in(v, self.reimager._inject_vars(v, '')))
75 self.assertTrue(find_all_in(v, self.reimager._inject_vars(v, 'ctrl')))
76
77
78 def testReportResultsGood(self):
79 """Should report results in the case where all jobs passed."""
80 job = self.mox.CreateMock(frontend.Job)
81 job.name = 'RPC Client job'
82 job.result = True
83 recorder = self.mox.CreateMock(base_job.base_job)
84 recorder.record('GOOD', mox.IgnoreArg(), job.name)
85 self.mox.ReplayAll()
86 self.reimager._report_results(job, recorder.record)
87
88
89 def testReportResultsBad(self):
90 """Should report results in various job failure cases.
91
92 In this test scenario, there are five hosts, all the 'netbook' platform.
93
94 h1: Did not run
95 h2: Two failed tests
96 h3: Two aborted tests
97 h4: completed, GOOD
98 h5: completed, GOOD
99 """
100 H1 = 'host1'
101 H2 = 'host2'
102 H3 = 'host3'
103 H4 = 'host4'
104 H5 = 'host5'
105
106 class FakeResult(object):
107 def __init__(self, reason):
108 self.reason = reason
109
110
111 # The RPC-client-side Job object that is annotated with results.
112 job = FakeJob()
113 job.result = None # job failed, there are results to report.
114
115 # The semantics of |results_platform_map| and |test_results| are
116 # drawn from frontend.AFE.poll_all_jobs()
117 job.results_platform_map = {'netbook': {'Aborted' : [H3],
118 'Completed' : [H1, H4, H5],
119 'Failed': [H2]
120 }
121 }
122 # Gin up fake results for H2 and H3 failure cases.
123 h2 = frontend.TestResults()
124 h2.fail = [FakeResult('a'), FakeResult('b')]
125 h3 = frontend.TestResults()
126 h3.fail = [FakeResult('a'), FakeResult('b')]
127 # Skipping H1 in |test_status| dict means that it did not get run.
128 job.test_status = {H2: h2, H3: h3, H4: {}, H5: {}}
129
130 # Set up recording expectations.
131 rjob = self.mox.CreateMock(base_job.base_job)
132 for res in h2.fail:
133 rjob.record('FAIL', mox.IgnoreArg(), H2, res.reason).InAnyOrder()
134 for res in h3.fail:
135 rjob.record('ABORT', mox.IgnoreArg(), H3, res.reason).InAnyOrder()
136 rjob.record('GOOD', mox.IgnoreArg(), H4).InAnyOrder()
137 rjob.record('GOOD', mox.IgnoreArg(), H5).InAnyOrder()
138 rjob.record(
139 'ERROR', mox.IgnoreArg(), H1, mox.IgnoreArg()).InAnyOrder()
140
141 self.mox.ReplayAll()
142 self.reimager._report_results(job, rjob.record)
143
144
145 def testScheduleJob(self):
146 """Should be able to create a job with the AFE."""
147 # Fake out getting the autoupdate control file contents.
148 cf_getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
149 cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
150 self.reimager._cf_getter = cf_getter
151
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800152 # Fake out getting the image URL pattern.
153 self.mox.StubOutWithMock(dynamic_suite, '_image_url_pattern')
154 dynamic_suite._image_url_pattern().AndReturn(self._URL)
155
Chris Masone6fed6462011-10-20 16:36:43 -0700156 self.afe.create_job(
157 control_file=mox.And(mox.StrContains(self._NAME),
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800158 mox.StrContains(self._URL % self._NAME)),
Chris Masone6fed6462011-10-20 16:36:43 -0700159 name=mox.StrContains(self._NAME),
160 control_type='Server',
161 meta_hosts=[self._BOARD] * self._NUM)
162 self.mox.ReplayAll()
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800163 self.reimager._schedule_reimage_job(self._NAME, self._NUM, self._BOARD)
Chris Masone6fed6462011-10-20 16:36:43 -0700164
165
166 def expect_attempt(self, success):
167 """Sets up |self.reimager| to expect an attempt() that returns |success|
168
169 @param success the value returned by poll_job_results()
170 @return a FakeJob configured with appropriate expectations
171 """
172 canary = FakeJob()
173 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
174 self.reimager._ensure_version_label(mox.StrContains(self._NAME))
175
176 self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800177 self.reimager._schedule_reimage_job(self._NAME,
Chris Masone6fed6462011-10-20 16:36:43 -0700178 self._NUM,
179 self._BOARD).AndReturn(canary)
180 if success is not None:
181 self.mox.StubOutWithMock(self.reimager, '_report_results')
182 self.reimager._report_results(canary, mox.IgnoreArg())
183
184 self.afe.get_jobs(id=canary.id, not_yet_run=True).AndReturn([])
185 self.afe.get_jobs(id=canary.id, finished=True).AndReturn([canary])
186 self.afe.poll_job_results(mox.IgnoreArg(), canary, 0).AndReturn(success)
187
188 return canary
189
190
191 def testSuccessfulReimage(self):
192 """Should attempt a reimage and record success."""
193 canary = self.expect_attempt(True)
194
195 rjob = self.mox.CreateMock(base_job.base_job)
196 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
197 rjob.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
198 self.mox.ReplayAll()
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800199 self.reimager.attempt(self._NAME, self._NUM, self._BOARD, rjob.record)
Chris Masone6fed6462011-10-20 16:36:43 -0700200
201
202 def testFailedReimage(self):
203 """Should attempt a reimage and record failure."""
204 canary = self.expect_attempt(False)
205
206 rjob = self.mox.CreateMock(base_job.base_job)
207 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
208 rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
209 self.mox.ReplayAll()
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800210 self.reimager.attempt(self._NAME, self._NUM, self._BOARD, rjob.record)
Chris Masone6fed6462011-10-20 16:36:43 -0700211
212
213 def testReimageThatNeverHappened(self):
214 """Should attempt a reimage and record that it didn't run."""
215 canary = self.expect_attempt(None)
216
217 rjob = self.mox.CreateMock(base_job.base_job)
218 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
219 rjob.record('FAIL', mox.IgnoreArg(), canary.name, mox.IgnoreArg())
220 rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
221 self.mox.ReplayAll()
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800222 self.reimager.attempt(self._NAME, self._NUM, self._BOARD, rjob.record)
Chris Masone6fed6462011-10-20 16:36:43 -0700223
224
225class SuiteTest(mox.MoxTestBase):
226 """Unit tests for dynamic_suite.Suite.
227
228 @var _NAME: fake image name
229 @var _TAG: fake suite tag
230 """
231
232 _NAME = 'name'
233 _TAG = 'suite tag'
234
235
236 def setUp(self):
237 class FakeControlData(object):
238 """A fake parsed control file data structure."""
239 def __init__(self, data, expr=False):
240 self.string = 'text-' + data
241 self.name = 'name-' + data
242 self.data = data
243 self.test_type = 'Client'
244 self.experimental = expr
245
246
247 super(SuiteTest, self).setUp()
248 self.afe = self.mox.CreateMock(frontend.AFE)
249 self.tko = self.mox.CreateMock(frontend.TKO)
250
251 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__)
252
253 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
254
255 self.files = {'one': FakeControlData('data_one', expr=True),
256 'two': FakeControlData('data_two'),
257 'three': FakeControlData('data_three')}
258
259
260 def tearDown(self):
261 super(SuiteTest, self).tearDown()
262 shutil.rmtree(self.tmpdir, ignore_errors=True)
263
264
265 def expect_control_file_parsing(self):
266 """Expect an attempt to parse the 'control files' in |self.files|."""
267 self.getter.get_control_file_list().AndReturn(self.files.keys())
268 self.mox.StubOutWithMock(control_data, 'parse_control_string')
269 for file, data in self.files.iteritems():
270 self.getter.get_control_file_contents(file).AndReturn(data.string)
271 control_data.parse_control_string(
272 data.string, raise_warnings=True).AndReturn(data)
273
274
275 def testFindAndParseStableTests(self):
276 """Should find only non-experimental tests that match a predicate."""
277 self.expect_control_file_parsing()
278 self.mox.ReplayAll()
279
280 predicate = lambda d: d.text == self.files['two'].string
281 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter, predicate)
282 self.assertEquals(len(tests), 1)
283 self.assertEquals(tests[0], self.files['two'])
284
285
286 def testFindAndParseTests(self):
287 """Should find all tests that match a predicate."""
288 self.expect_control_file_parsing()
289 self.mox.ReplayAll()
290
291 predicate = lambda d: d.text != self.files['two'].string
292 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter,
293 predicate,
294 add_experimental=True)
295 self.assertEquals(len(tests), 2)
296 self.assertTrue(self.files['one'] in tests)
297 self.assertTrue(self.files['three'] in tests)
298
299
300 def mock_control_file_parsing(self):
301 """Fake out find_and_parse_tests(), returning content from |self.files|.
302 """
303 for test in self.files.values():
304 test.text = test.string # mimic parsing.
305 self.mox.StubOutWithMock(dynamic_suite.Suite, 'find_and_parse_tests')
306 dynamic_suite.Suite.find_and_parse_tests(
307 mox.IgnoreArg(),
308 mox.IgnoreArg(),
309 add_experimental=True).AndReturn(self.files.values())
310
311
312 def testStableUnstableFilter(self):
313 """Should distinguish between experimental and stable tests."""
314 self.mock_control_file_parsing()
315 self.mox.ReplayAll()
316 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
317 self.afe, self.tko)
318
319 self.assertTrue(self.files['one'] in suite.tests)
320 self.assertTrue(self.files['two'] in suite.tests)
321 self.assertTrue(self.files['one'] in suite.unstable_tests())
322 self.assertTrue(self.files['two'] in suite.stable_tests())
323 self.assertFalse(self.files['one'] in suite.stable_tests())
324 self.assertFalse(self.files['two'] in suite.unstable_tests())
325
326
327 def expect_job_scheduling(self, add_experimental):
328 """Expect jobs to be scheduled for 'tests' in |self.files|.
329
330 @param add_experimental: expect jobs for experimental tests as well.
331 """
332 for test in self.files.values():
333 if not add_experimental and test.experimental:
334 continue
335 self.afe.create_job(
336 control_file=test.text,
337 name=mox.And(mox.StrContains(self._NAME),
338 mox.StrContains(test.name)),
339 control_type=mox.IgnoreArg(),
340 meta_hosts=[dynamic_suite.VERSION_PREFIX+self._NAME])
341
342
343 def testScheduleTests(self):
344 """Should schedule stable and experimental tests with the AFE."""
345 self.mock_control_file_parsing()
346 self.expect_job_scheduling(add_experimental=True)
347
348 self.mox.ReplayAll()
349 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
350 self.afe, self.tko)
351 suite.schedule(self._NAME)
352
353
354 def testScheduleStableTests(self):
355 """Should schedule only stable tests with the AFE."""
356 self.mock_control_file_parsing()
357 self.expect_job_scheduling(add_experimental=False)
358
359 self.mox.ReplayAll()
360 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
361 self.afe, self.tko)
362 suite.schedule(self._NAME, add_experimental=False)
363
364
365 def expect_result_gathering(self, job):
366 self.afe.get_jobs(id=job.id, finished=True).AndReturn(job)
367 entries = map(lambda s: s.entry, job.statuses)
368 self.afe.run('get_host_queue_entries',
369 job=job.id).AndReturn(entries)
370 if True not in map(lambda e: 'aborted' in e and e['aborted'], entries):
371 self.tko.get_status_counts(job=job.id).AndReturn(job.statuses)
372
373
374 def testWaitForResults(self):
375 """Should gather status and return records for job summaries."""
376 class FakeStatus(object):
377 """Fake replacement for server-side job status objects.
378
379 @var status: 'GOOD', 'FAIL', 'ERROR', etc.
380 @var test_name: name of the test this is status for
381 @var reason: reason for failure, if any
382 @var aborted: present and True if the job was aborted. Optional.
383 """
384 def __init__(self, code, name, reason, aborted=None):
385 self.status = code
386 self.test_name = name
387 self.reason = reason
388 self.entry = {}
389 if aborted:
390 self.entry['aborted'] = True
391
392 def equals_record(self, args):
393 """Compares this object to a recorded status."""
394 return self._equals_record(*args)
395
Chris Masone2ef1d4e2011-12-20 11:06:53 -0800396 def _equals_record(self, status, subdir, name, reason=None):
Chris Masone6fed6462011-10-20 16:36:43 -0700397 """Compares this object and fields of recorded status."""
398 if 'aborted' in self.entry and self.entry['aborted']:
399 return status == 'ABORT'
400 return (self.status == status and
401 self.test_name == name and
402 self.reason == reason)
403
404
405 jobs = [FakeJob(0, [FakeStatus('GOOD', 'T0', ''),
406 FakeStatus('GOOD', 'T1', '')]),
407 FakeJob(1, [FakeStatus('ERROR', 'T0', 'err', False),
408 FakeStatus('GOOD', 'T1', '')]),
409 FakeJob(2, [FakeStatus('TEST_NA', 'T0', 'no')]),
410 FakeJob(2, [FakeStatus('FAIL', 'T0', 'broken')]),
411 FakeJob(3, [FakeStatus('ERROR', 'T0', 'gah', True)])]
412 # To simulate a job that isn't ready the first time we check.
413 self.afe.get_jobs(id=jobs[0].id, finished=True).AndReturn([])
414 # Expect all the rest of the jobs to be good to go the first time.
415 for job in jobs[1:]:
416 self.expect_result_gathering(job)
417 # Then, expect job[0] to be ready.
418 self.expect_result_gathering(jobs[0])
419 # Expect us to poll twice.
420 self.mox.StubOutWithMock(time, 'sleep')
421 time.sleep(5)
422 time.sleep(5)
423 self.mox.ReplayAll()
424
425 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
426 self.afe, self.tko)
427 suite._jobs = list(jobs)
428 results = suite.wait_for_results()
429 for job in jobs:
430 for status in job.statuses:
431 self.assertTrue(True in map(status.equals_record, results))
432
433
434 def testRunAndWaitSuccess(self):
435 """Should record successful results."""
436 results = [('GOOD', None, 'good'), ('FAIL', None, 'bad', 'reason')]
437
438 recorder = self.mox.CreateMock(base_job.base_job)
439 recorder.record('START', mox.IgnoreArg(), self._TAG)
440 for result in results:
441 recorder.record(*result).InAnyOrder('results')
442 recorder.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
443
444 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
445 self.afe, self.tko)
446 self.mox.StubOutWithMock(suite, 'schedule')
447 suite.schedule(self._NAME, True)
448 self.mox.StubOutWithMock(suite, 'wait_for_results')
449 suite.wait_for_results().AndReturn(results)
450 self.mox.ReplayAll()
451
452 suite.run_and_wait(self._NAME, recorder.record, True)
453
454
455 def testRunAndWaitFailure(self):
456 """Should record failure to gather results."""
457 recorder = self.mox.CreateMock(base_job.base_job)
458 recorder.record('START', mox.IgnoreArg(), self._TAG)
459 recorder.record('END ERROR', None, None, mox.StrContains('waiting'))
460
461 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
462 self.afe, self.tko)
463 self.mox.StubOutWithMock(suite, 'schedule')
464 suite.schedule(self._NAME, True)
465 self.mox.StubOutWithMock(suite, 'wait_for_results')
466 suite.wait_for_results().AndRaise(Exception())
467 self.mox.ReplayAll()
468
469 suite.run_and_wait(self._NAME, recorder.record, True)
470
471
472 def testRunAndWaitScheduleFailure(self):
473 """Should record failure to gather results."""
474 recorder = self.mox.CreateMock(base_job.base_job)
475 recorder.record('START', mox.IgnoreArg(), self._TAG)
476 recorder.record('END ERROR', None, None, mox.StrContains('scheduling'))
477
478 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
479 self.afe, self.tko)
480 self.mox.StubOutWithMock(suite, 'schedule')
481 suite.schedule(self._NAME, True).AndRaise(Exception())
482 self.mox.ReplayAll()
483
484 suite.run_and_wait(self._NAME, recorder.record, True)
485
486
487if __name__ == '__main__':
488 unittest.main()