blob: 226657e12166d8cb4a26fe301ba327bb35f02c93 [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
37 _URL = 'http://nothing'
38 _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
152 self.afe.create_job(
153 control_file=mox.And(mox.StrContains(self._NAME),
154 mox.StrContains(self._URL)),
155 name=mox.StrContains(self._NAME),
156 control_type='Server',
157 meta_hosts=[self._BOARD] * self._NUM)
158 self.mox.ReplayAll()
159 self.reimager._schedule_reimage_job(self._URL, self._NAME,
160 self._NUM, self._BOARD)
161
162
163 def expect_attempt(self, success):
164 """Sets up |self.reimager| to expect an attempt() that returns |success|
165
166 @param success the value returned by poll_job_results()
167 @return a FakeJob configured with appropriate expectations
168 """
169 canary = FakeJob()
170 self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
171 self.reimager._ensure_version_label(mox.StrContains(self._NAME))
172
173 self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
174 self.reimager._schedule_reimage_job(self._URL,
175 self._NAME,
176 self._NUM,
177 self._BOARD).AndReturn(canary)
178 if success is not None:
179 self.mox.StubOutWithMock(self.reimager, '_report_results')
180 self.reimager._report_results(canary, mox.IgnoreArg())
181
182 self.afe.get_jobs(id=canary.id, not_yet_run=True).AndReturn([])
183 self.afe.get_jobs(id=canary.id, finished=True).AndReturn([canary])
184 self.afe.poll_job_results(mox.IgnoreArg(), canary, 0).AndReturn(success)
185
186 return canary
187
188
189 def testSuccessfulReimage(self):
190 """Should attempt a reimage and record success."""
191 canary = self.expect_attempt(True)
192
193 rjob = self.mox.CreateMock(base_job.base_job)
194 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
195 rjob.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
196 self.mox.ReplayAll()
197 self.reimager.attempt(self._URL, self._NAME,
198 self._NUM, self._BOARD, rjob.record)
199
200
201 def testFailedReimage(self):
202 """Should attempt a reimage and record failure."""
203 canary = self.expect_attempt(False)
204
205 rjob = self.mox.CreateMock(base_job.base_job)
206 rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
207 rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
208 self.mox.ReplayAll()
209 self.reimager.attempt(self._URL, self._NAME,
210 self._NUM, self._BOARD, rjob.record)
211
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()
222 self.reimager.attempt(self._URL, self._NAME,
223 self._NUM, self._BOARD, rjob.record)
224
225
226class SuiteTest(mox.MoxTestBase):
227 """Unit tests for dynamic_suite.Suite.
228
229 @var _NAME: fake image name
230 @var _TAG: fake suite tag
231 """
232
233 _NAME = 'name'
234 _TAG = 'suite tag'
235
236
237 def setUp(self):
238 class FakeControlData(object):
239 """A fake parsed control file data structure."""
240 def __init__(self, data, expr=False):
241 self.string = 'text-' + data
242 self.name = 'name-' + data
243 self.data = data
244 self.test_type = 'Client'
245 self.experimental = expr
246
247
248 super(SuiteTest, self).setUp()
249 self.afe = self.mox.CreateMock(frontend.AFE)
250 self.tko = self.mox.CreateMock(frontend.TKO)
251
252 self.tmpdir = tempfile.mkdtemp(suffix=type(self).__name__)
253
254 self.getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
255
256 self.files = {'one': FakeControlData('data_one', expr=True),
257 'two': FakeControlData('data_two'),
258 'three': FakeControlData('data_three')}
259
260
261 def tearDown(self):
262 super(SuiteTest, self).tearDown()
263 shutil.rmtree(self.tmpdir, ignore_errors=True)
264
265
266 def expect_control_file_parsing(self):
267 """Expect an attempt to parse the 'control files' in |self.files|."""
268 self.getter.get_control_file_list().AndReturn(self.files.keys())
269 self.mox.StubOutWithMock(control_data, 'parse_control_string')
270 for file, data in self.files.iteritems():
271 self.getter.get_control_file_contents(file).AndReturn(data.string)
272 control_data.parse_control_string(
273 data.string, raise_warnings=True).AndReturn(data)
274
275
276 def testFindAndParseStableTests(self):
277 """Should find only non-experimental tests that match a predicate."""
278 self.expect_control_file_parsing()
279 self.mox.ReplayAll()
280
281 predicate = lambda d: d.text == self.files['two'].string
282 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter, predicate)
283 self.assertEquals(len(tests), 1)
284 self.assertEquals(tests[0], self.files['two'])
285
286
287 def testFindAndParseTests(self):
288 """Should find all tests that match a predicate."""
289 self.expect_control_file_parsing()
290 self.mox.ReplayAll()
291
292 predicate = lambda d: d.text != self.files['two'].string
293 tests = dynamic_suite.Suite.find_and_parse_tests(self.getter,
294 predicate,
295 add_experimental=True)
296 self.assertEquals(len(tests), 2)
297 self.assertTrue(self.files['one'] in tests)
298 self.assertTrue(self.files['three'] in tests)
299
300
301 def mock_control_file_parsing(self):
302 """Fake out find_and_parse_tests(), returning content from |self.files|.
303 """
304 for test in self.files.values():
305 test.text = test.string # mimic parsing.
306 self.mox.StubOutWithMock(dynamic_suite.Suite, 'find_and_parse_tests')
307 dynamic_suite.Suite.find_and_parse_tests(
308 mox.IgnoreArg(),
309 mox.IgnoreArg(),
310 add_experimental=True).AndReturn(self.files.values())
311
312
313 def testStableUnstableFilter(self):
314 """Should distinguish between experimental and stable tests."""
315 self.mock_control_file_parsing()
316 self.mox.ReplayAll()
317 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
318 self.afe, self.tko)
319
320 self.assertTrue(self.files['one'] in suite.tests)
321 self.assertTrue(self.files['two'] in suite.tests)
322 self.assertTrue(self.files['one'] in suite.unstable_tests())
323 self.assertTrue(self.files['two'] in suite.stable_tests())
324 self.assertFalse(self.files['one'] in suite.stable_tests())
325 self.assertFalse(self.files['two'] in suite.unstable_tests())
326
327
328 def expect_job_scheduling(self, add_experimental):
329 """Expect jobs to be scheduled for 'tests' in |self.files|.
330
331 @param add_experimental: expect jobs for experimental tests as well.
332 """
333 for test in self.files.values():
334 if not add_experimental and test.experimental:
335 continue
336 self.afe.create_job(
337 control_file=test.text,
338 name=mox.And(mox.StrContains(self._NAME),
339 mox.StrContains(test.name)),
340 control_type=mox.IgnoreArg(),
341 meta_hosts=[dynamic_suite.VERSION_PREFIX+self._NAME])
342
343
344 def testScheduleTests(self):
345 """Should schedule stable and experimental tests with the AFE."""
346 self.mock_control_file_parsing()
347 self.expect_job_scheduling(add_experimental=True)
348
349 self.mox.ReplayAll()
350 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
351 self.afe, self.tko)
352 suite.schedule(self._NAME)
353
354
355 def testScheduleStableTests(self):
356 """Should schedule only stable tests with the AFE."""
357 self.mock_control_file_parsing()
358 self.expect_job_scheduling(add_experimental=False)
359
360 self.mox.ReplayAll()
361 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
362 self.afe, self.tko)
363 suite.schedule(self._NAME, add_experimental=False)
364
365
366 def expect_result_gathering(self, job):
367 self.afe.get_jobs(id=job.id, finished=True).AndReturn(job)
368 entries = map(lambda s: s.entry, job.statuses)
369 self.afe.run('get_host_queue_entries',
370 job=job.id).AndReturn(entries)
371 if True not in map(lambda e: 'aborted' in e and e['aborted'], entries):
372 self.tko.get_status_counts(job=job.id).AndReturn(job.statuses)
373
374
375 def testWaitForResults(self):
376 """Should gather status and return records for job summaries."""
377 class FakeStatus(object):
378 """Fake replacement for server-side job status objects.
379
380 @var status: 'GOOD', 'FAIL', 'ERROR', etc.
381 @var test_name: name of the test this is status for
382 @var reason: reason for failure, if any
383 @var aborted: present and True if the job was aborted. Optional.
384 """
385 def __init__(self, code, name, reason, aborted=None):
386 self.status = code
387 self.test_name = name
388 self.reason = reason
389 self.entry = {}
390 if aborted:
391 self.entry['aborted'] = True
392
393 def equals_record(self, args):
394 """Compares this object to a recorded status."""
395 return self._equals_record(*args)
396
397 def _equals_record(self, status, subdir, name, reason):
398 """Compares this object and fields of recorded status."""
399 if 'aborted' in self.entry and self.entry['aborted']:
400 return status == 'ABORT'
401 return (self.status == status and
402 self.test_name == name and
403 self.reason == reason)
404
405
406 jobs = [FakeJob(0, [FakeStatus('GOOD', 'T0', ''),
407 FakeStatus('GOOD', 'T1', '')]),
408 FakeJob(1, [FakeStatus('ERROR', 'T0', 'err', False),
409 FakeStatus('GOOD', 'T1', '')]),
410 FakeJob(2, [FakeStatus('TEST_NA', 'T0', 'no')]),
411 FakeJob(2, [FakeStatus('FAIL', 'T0', 'broken')]),
412 FakeJob(3, [FakeStatus('ERROR', 'T0', 'gah', True)])]
413 # To simulate a job that isn't ready the first time we check.
414 self.afe.get_jobs(id=jobs[0].id, finished=True).AndReturn([])
415 # Expect all the rest of the jobs to be good to go the first time.
416 for job in jobs[1:]:
417 self.expect_result_gathering(job)
418 # Then, expect job[0] to be ready.
419 self.expect_result_gathering(jobs[0])
420 # Expect us to poll twice.
421 self.mox.StubOutWithMock(time, 'sleep')
422 time.sleep(5)
423 time.sleep(5)
424 self.mox.ReplayAll()
425
426 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
427 self.afe, self.tko)
428 suite._jobs = list(jobs)
429 results = suite.wait_for_results()
430 for job in jobs:
431 for status in job.statuses:
432 self.assertTrue(True in map(status.equals_record, results))
433
434
435 def testRunAndWaitSuccess(self):
436 """Should record successful results."""
437 results = [('GOOD', None, 'good'), ('FAIL', None, 'bad', 'reason')]
438
439 recorder = self.mox.CreateMock(base_job.base_job)
440 recorder.record('START', mox.IgnoreArg(), self._TAG)
441 for result in results:
442 recorder.record(*result).InAnyOrder('results')
443 recorder.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
444
445 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
446 self.afe, self.tko)
447 self.mox.StubOutWithMock(suite, 'schedule')
448 suite.schedule(self._NAME, True)
449 self.mox.StubOutWithMock(suite, 'wait_for_results')
450 suite.wait_for_results().AndReturn(results)
451 self.mox.ReplayAll()
452
453 suite.run_and_wait(self._NAME, recorder.record, True)
454
455
456 def testRunAndWaitFailure(self):
457 """Should record failure to gather results."""
458 recorder = self.mox.CreateMock(base_job.base_job)
459 recorder.record('START', mox.IgnoreArg(), self._TAG)
460 recorder.record('END ERROR', None, None, mox.StrContains('waiting'))
461
462 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
463 self.afe, self.tko)
464 self.mox.StubOutWithMock(suite, 'schedule')
465 suite.schedule(self._NAME, True)
466 self.mox.StubOutWithMock(suite, 'wait_for_results')
467 suite.wait_for_results().AndRaise(Exception())
468 self.mox.ReplayAll()
469
470 suite.run_and_wait(self._NAME, recorder.record, True)
471
472
473 def testRunAndWaitScheduleFailure(self):
474 """Should record failure to gather results."""
475 recorder = self.mox.CreateMock(base_job.base_job)
476 recorder.record('START', mox.IgnoreArg(), self._TAG)
477 recorder.record('END ERROR', None, None, mox.StrContains('scheduling'))
478
479 suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
480 self.afe, self.tko)
481 self.mox.StubOutWithMock(suite, 'schedule')
482 suite.schedule(self._NAME, True).AndRaise(Exception())
483 self.mox.ReplayAll()
484
485 suite.run_and_wait(self._NAME, recorder.record, True)
486
487
488if __name__ == '__main__':
489 unittest.main()