blob: 40de80a1298ed3b0c69dd6a3671e7ad6c5457622 [file] [log] [blame]
#!/usr/bin/python
#
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for server/cros/dynamic_suite/reimager.py."""
import mox
from autotest_lib.client.common_lib import base_job, error
from autotest_lib.client.common_lib import global_config
from autotest_lib.client.common_lib.cros import dev_server
from autotest_lib.frontend.afe.json_rpc import proxy
from autotest_lib.server.cros.dynamic_suite import control_file_getter
from autotest_lib.server.cros.dynamic_suite import host_spec
from autotest_lib.server.cros.dynamic_suite import job_status, tools
from autotest_lib.server.cros.dynamic_suite.comparitors import StatusContains
from autotest_lib.server.cros.dynamic_suite.host_spec import ExplicitHostGroup
from autotest_lib.server.cros.dynamic_suite.host_spec import HostGroup
from autotest_lib.server.cros.dynamic_suite.host_spec import HostSpec
from autotest_lib.server.cros.dynamic_suite.host_spec import MetaHostGroup
from autotest_lib.server.cros.dynamic_suite.reimager import OsReimager
from autotest_lib.server.cros.dynamic_suite.fakes import FakeHost, FakeJob
from autotest_lib.server import frontend
class ReimagerTest(mox.MoxTestBase):
"""Unit tests for dynamic_suite Reimager class.
@var _URL: fake image url
@var _BUILD: fake build
@var _NUM: fake number of machines to run on
@var _BOARD: fake board to reimage
"""
_DEVSERVER_URL = 'http://nothing:8082'
_BUILD = 'build'
_UPDATE_URL = _DEVSERVER_URL + '/update/' + _BUILD
_URL = '%s/%s'
_NUM = 4
_BOARD = 'board'
_POOL = 'bvt'
_DEPENDENCIES = {'test1': ['label1'], 'test2': ['label2']}
_CONFIG = global_config.global_config
def setUp(self):
super(ReimagerTest, self).setUp()
self.afe = self.mox.CreateMock(frontend.AFE)
self.tko = self.mox.CreateMock(frontend.TKO)
self.devserver = dev_server.ImageServer(self._DEVSERVER_URL)
self.reimager = OsReimager('', self._BOARD, afe=self.afe, tko=self.tko)
# Having these ordered by complexity is important!
host_spec_list = [HostSpec([self._BOARD, self._POOL])]
for dep_list in self._DEPENDENCIES.itervalues():
host_spec_list.append(
HostSpec([self._BOARD, self._POOL], dep_list))
self.specs = host_spec.order_by_complexity(host_spec_list)
self._CONFIG.override_config_value('CROS',
'sharding_factor',
"%d" % self._NUM)
def check_specs(self, specs, expected):
"""
Asserts that for each entry x in the list specs, x.labels is
in the list expected.
@param specs: A list of specs to be checked.
@param expected: A list of expected labels values.
"""
for labels in expected:
labels.sort()
for spec in specs:
self.assertTrue(spec.labels in expected,
'%r not in %r' % (spec.labels, expected))
def testBuildHostSpecs(self):
"""Should uniquify a dict of test deps into a list of host specs."""
base_expectations = [['label1', 'label2'], ['label1'], []]
dependencies = {'a': base_expectations[2],
'b': sorted(base_expectations[0], reverse=True),
'c': base_expectations[1],
'd': base_expectations[0],
'e': base_expectations[1],
}
per_test_specs = self.reimager._build_host_specs_from_dependencies(
self._BOARD, None, dependencies)
self.check_specs(set(per_test_specs.values()),
[d + [self._BOARD] for d in base_expectations])
per_test_specs = self.reimager._build_host_specs_from_dependencies(
self._BOARD, self._POOL, dependencies)
self.check_specs(
set(per_test_specs.values()),
[d + [self._BOARD, self._POOL] for d in base_expectations])
def testBuildHostSpecsFromEmptyDict(self):
"""Should tolerate an empty dict of test deps."""
per_test_specs = self.reimager._build_host_specs_from_dependencies(
self._BOARD, None, {'':[]})
specs = set(per_test_specs.values())
self.assertEquals(len(specs), 1)
self.assertEquals(specs.pop().labels, [self._BOARD])
def testEnsureVersionLabelAlreadyExists(self):
"""Should tolerate a label that already exists."""
name = 'label'
error = proxy.ValidationError(
{'name': 'ValidationError',
'message': '{"name": "This value must be unique"}',
'traceback': ''},
'BAD')
self.afe.create_label(name=name).AndRaise(error)
self.mox.ReplayAll()
self.reimager._ensure_version_label(name)
def testEnsureVersionLabel(self):
"""Should create a label if it doesn't already exist."""
name = 'label'
self.afe.create_label(name=name)
self.mox.ReplayAll()
self.reimager._ensure_version_label(name)
def testGatherHostsFromSpecs(self):
"""Get at least one host per spec."""
host_lists = [['h1'], ['h2', 'h3'], ['h1', 'h2', 'h4']]
spec_host_pairs = zip(self.specs, host_lists)
for spec, hosts in spec_host_pairs:
self.afe.get_hosts(
multiple_labels=mox.SameElementsAs(spec.labels)
).AndReturn(hosts)
self.mox.ReplayAll()
self.assertEquals(
dict(spec_host_pairs),
self.reimager._gather_hosts_from_host_specs(self.specs))
def testBuildHostGroupComplex(self):
"""Build a HostGroup from hosts, given a complex set of HostSpec."""
self.mox.StubOutWithMock(self.reimager, '_choose_hosts')
self.mox.StubOutWithMock(self.reimager, '_gather_hosts_from_host_specs')
require_usable_hosts = True
host_lists = [[FakeHost('h%d' % i)] for i,spec in enumerate(self.specs)]
hosts_per_spec = dict(zip(self.specs, host_lists))
host_group = ExplicitHostGroup(hosts_per_spec)
self.reimager._gather_hosts_from_host_specs(
self.specs).AndReturn(hosts_per_spec)
self.reimager._choose_hosts(
hosts_per_spec,
len(self.specs),
require_usable_hosts).AndReturn(host_group)
self.mox.ReplayAll()
self.assertEquals(host_group,
self.reimager._build_host_group(self.specs,
len(self.specs),
require_usable_hosts))
def testBuildHostGroupComplexContainingTrivial(self):
"""Build a HostGroup, given a complex set of specs, with one trivial."""
self.mox.StubOutWithMock(self.reimager, '_choose_hosts')
self.mox.StubOutWithMock(self.reimager, '_gather_hosts_from_host_specs')
require_usable_hosts = True
host_lists = [[FakeHost('h%d' % i)] for i,spec in enumerate(self.specs)]
hosts_per_spec = dict(zip(self.specs, host_lists))
host_group = ExplicitHostGroup(hosts_per_spec)
self.reimager._gather_hosts_from_host_specs(
self.specs).AndReturn(hosts_per_spec)
self.reimager._choose_hosts(
hosts_per_spec,
len(self.specs) - 1,
require_usable_hosts).AndReturn(host_group)
self.mox.ReplayAll()
self.assertEquals(host_group,
self.reimager._build_host_group(self.specs,
len(self.specs) - 1,
require_usable_hosts))
def testBuildHostGroupNotEnough(self):
"""Raise if more nontrivial HostSpecs than machines allowed."""
specs = [HostSpec(['l1'], ['e1']), HostSpec(['21'], ['e1'])]
self.assertRaises(error.InadequateHostsException,
self.reimager._build_host_group,
specs,
len(specs) - 1,
True)
def testBuildHostGroupSimple(self):
"""Build a HostGroup from labels, given a simple set of HostSpec."""
spec = self.specs[0]
host_list = [FakeHost(), FakeHost()]
self.afe.get_hosts(multiple_labels=spec.labels).AndReturn(host_list)
host_group = MetaHostGroup(spec.labels, len(host_list))
self.mox.StubOutWithMock(self.reimager, '_choose_hosts')
self.mox.ReplayAll()
self.assertTrue(host_group,
self.reimager._build_host_group([spec],
len(host_list),
True))
def testBuildHostGroupSimpleNone(self):
"""Raise when we find no machines to match a simple set of HostSpec."""
spec = self.specs[0]
self.afe.get_hosts(multiple_labels=spec.labels).AndReturn([])
self.mox.StubOutWithMock(self.reimager, '_choose_hosts')
self.mox.ReplayAll()
self.assertRaises(error.NoHostsException,
self.reimager._build_host_group,
[spec],
1,
True)
def testScheduleJob(self):
"""Should be able to create a job with the AFE."""
# Fake out getting the autoupdate control file contents.
cf_getter = self.mox.CreateMock(control_file_getter.ControlFileGetter)
cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
self.reimager._cf_getter = cf_getter
hosts_per_spec = {HostSpec(['l1']): [FakeHost('h1')],
HostSpec(['l2']): [FakeHost('h2')],
HostSpec(['l3']): [FakeHost('h4')]}
hostnames = [h[0].hostname for h in hosts_per_spec.values()]
self.afe.create_job(
control_file=mox.And(
mox.StrContains(self._BUILD),
mox.StrContains(self._UPDATE_URL)),
name=mox.StrContains(self._BUILD),
control_type='Server',
hosts=mox.SameElementsAs(hostnames),
priority='Low',
parent_job_id=None)
self.mox.ReplayAll()
self.reimager._schedule_reimage_job(
{'image_name': self._BUILD},
ExplicitHostGroup(hosts_per_spec), self.devserver)
def testPackageUrl(self):
"""Should be able to get the package_url for any build."""
self._CONFIG.override_config_value('CROS',
'package_url_pattern',
self._URL)
self.mox.ReplayAll()
package_url = tools.get_package_url(self._DEVSERVER_URL, self._BUILD)
self.assertEqual(package_url, self._URL % (self._DEVSERVER_URL,
self._BUILD))
def expect_attempt(self, canary_job, statuses, ex=None, check_hosts=True,
unsatisfiable_specs=[], doomed_specs=[],
tryjob_aborted=False):
"""Sets up |self.reimager| to expect an attempt().
The return value of attempt() is dictated by the aggregate of the
status values in |statuses|; if all are GOOD, then attempt() will
return True. Otherwise, False -- just like the real call.
Also stubs out Reimager._clear_build_state(), should the caller wish
to set an expectation there as well.
@param canary_job: a FakeJob representing the job we're expecting.
@param statuses: dict mapping a hostname to its job_status.Status.
Will be returned by job_status.gather_per_host_results
@param ex: if not None, |ex| is raised by get_jobs()
@param check_hosts: expected check_hosts argument
@param unsatisfiable_specs: desired host_group.unsatisfiable_specs
@param doomed_specs: desired host_group.doomed_specs
@param tryjob_aborted: report that too many tryjobs aborted
@return a FakeJob configured with appropriate expectations
"""
self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
self.mox.StubOutWithMock(self.reimager, '_build_host_group')
self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job_base')
self.mox.StubOutWithMock(self.reimager, '_clear_build_state')
self.mox.StubOutWithMock(job_status, 'wait_for_jobs_to_start')
self.mox.StubOutWithMock(job_status, 'wait_for_and_lock_job_hosts')
self.mox.StubOutWithMock(job_status, 'gather_job_hostnames')
self.mox.StubOutWithMock(job_status, 'wait_for_jobs_to_finish')
self.mox.StubOutWithMock(job_status, 'gather_per_host_results')
self.mox.StubOutWithMock(job_status, 'check_and_record_reimage_results')
self.mox.StubOutWithMock(job_status, 'check_job_abort_status')
self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
host_group = self.mox.CreateMock(HostGroup)
host_group.unsatisfied_specs = unsatisfiable_specs
host_group.doomed_specs = doomed_specs
self.reimager._build_host_group(
mox.IgnoreArg(), self._NUM, check_hosts).AndReturn(host_group)
self.reimager._schedule_reimage_job(
{'image_name': self._BUILD},
host_group,
self.devserver).AndReturn(canary_job)
job_status.wait_for_jobs_to_start(self.afe, [canary_job],
start_time=mox.IgnoreArg(),
wait_timeout_mins=mox.IgnoreArg()).AndReturn(True)
if ex:
job_status.wait_for_jobs_to_finish(self.afe,
[canary_job]).AndRaise(ex)
return
else:
job_status.wait_for_jobs_to_finish(self.afe, [canary_job])
job_status.check_job_abort_status(mox.IgnoreArg(), mox.IgnoreArg()
).AndReturn(tryjob_aborted)
if tryjob_aborted:
return
job_status.gather_per_host_results(
mox.IgnoreArg(), mox.IgnoreArg(), [canary_job],
mox.StrContains(OsReimager.JOB_NAME)).AndReturn(statuses)
if statuses:
ret_val = reduce(lambda v, s: v or s.is_good(),
statuses.values(), False)
job_status.check_and_record_reimage_results(
statuses, host_group, mox.IgnoreArg()).AndReturn(ret_val)
def testSuccessfulReimage(self):
"""Should attempt a reimage and record success."""
canary = FakeJob()
statuses = {canary.hostnames[0]:
job_status.Status('GOOD', canary.hostnames[0])}
self.expect_attempt(canary, statuses)
rjob = self.mox.CreateMock(base_job.base_job)
self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
self.mox.ReplayAll()
self.assertTrue(self.reimager.attempt(self._BUILD, self._POOL,
self.devserver,
rjob.record_entry, True,
[], self._DEPENDENCIES))
self.reimager.clear_reimaged_host_state(self._BUILD)
def testTryJobAborted(self):
"""Should attempt a reimage that aborts and record ABORT"""
canary = FakeJob()
statuses = {canary.hostnames[0]:
job_status.Status('GOOD', canary.hostnames[0])}
self.expect_attempt(canary, statuses, tryjob_aborted=True)
rjob = self.mox.CreateMock(base_job.base_job)
rjob.record_entry(StatusContains.CreateFromStrings('START'))
rjob.record_entry(StatusContains.CreateFromStrings('ABORT'))
rjob.record_entry(StatusContains.CreateFromStrings('END ABORT'))
self.mox.ReplayAll()
self.assertFalse(self.reimager.attempt(self._BUILD, self._POOL,
self.devserver,
rjob.record_entry, True, [],
self._DEPENDENCIES))
self.reimager.clear_reimaged_host_state(self._BUILD)
def testSuccessfulReimageByMetahost(self):
"""Should attempt a reimage by metahost and record success."""
canary = FakeJob()
statuses = {canary.hostnames[0]: job_status.Status('GOOD',
canary.hostnames[0])}
self.expect_attempt(canary, statuses)
rjob = self.mox.CreateMock(base_job.base_job)
self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
self.mox.ReplayAll()
self.assertTrue(self.reimager.attempt(self._BUILD, self._POOL,
self.devserver,
rjob.record_entry, True, []))
self.reimager.clear_reimaged_host_state(self._BUILD)
def testPartialReimage(self):
"""Should attempt a reimage with failing hosts and record success."""
canary = FakeJob(hostnames=['host1', 'host2'])
statuses = {
canary.hostnames[0]: job_status.Status('FAIL', canary.hostnames[0]),
canary.hostnames[1]: job_status.Status('GOOD', canary.hostnames[1]),
}
self.expect_attempt(canary, statuses)
rjob = self.mox.CreateMock(base_job.base_job)
comparator = mox.Or(mox.StrContains('host1'), mox.StrContains('host2'))
self.reimager._clear_build_state(comparator)
self.reimager._clear_build_state(comparator)
self.mox.ReplayAll()
self.assertTrue(self.reimager.attempt(self._BUILD, None,
self.devserver,
rjob.record_entry, True, []))
self.reimager.clear_reimaged_host_state(self._BUILD)
def testPartialReimageWithDependencies(self):
"""Attempt a reimage with unsatisfied deps and report tests to skip."""
canary = FakeJob()
statuses = {canary.hostnames[0]: job_status.Status('GOOD',
canary.hostnames[0])}
bad_test, bad_labels = self._DEPENDENCIES.items()[0]
bad_spec = HostSpec([self._BOARD, self._POOL], bad_labels)
self.expect_attempt(canary, statuses, unsatisfiable_specs=[bad_spec])
rjob = self.mox.CreateMock(base_job.base_job)
rjob.record_entry(StatusContains.CreateFromStrings('START'))
rjob.record_entry(StatusContains.CreateFromStrings('TEST_NA', bad_test))
rjob.record_entry(StatusContains.CreateFromStrings('END TEST_NA'))
self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
self.mox.ReplayAll()
tests_to_skip = []
self.assertTrue(self.reimager.attempt(self._BUILD, self._POOL,
self.devserver,
rjob.record_entry, True,
tests_to_skip,
self._DEPENDENCIES))
self.reimager.clear_reimaged_host_state(self._BUILD)
self.assertTrue(bad_test in tests_to_skip)
def testPartialFailedReimageWithDependencies(self):
"""Attempt a reimage with failing hosts, ERROR on unrunnable tests."""
canary = FakeJob(hostnames=['host1', 'host2'])
statuses = {
canary.hostnames[0]: job_status.Status('FAIL', canary.hostnames[0]),
canary.hostnames[1]: job_status.Status('GOOD', canary.hostnames[1]),
}
bad_test, bad_labels = self._DEPENDENCIES.items()[0]
bad_spec = HostSpec([self._BOARD, self._POOL], bad_labels)
self.expect_attempt(canary, statuses, doomed_specs=[bad_spec])
rjob = self.mox.CreateMock(base_job.base_job)
rjob.record_entry(StatusContains.CreateFromStrings('START'))
rjob.record_entry(StatusContains.CreateFromStrings('ERROR', bad_test))
rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
comparator = mox.Or(mox.StrContains(canary.hostnames[0]),
mox.StrContains(canary.hostnames[1]))
self.reimager._clear_build_state(comparator)
self.reimager._clear_build_state(comparator)
self.mox.ReplayAll()
tests_to_skip = []
self.assertTrue(self.reimager.attempt(self._BUILD, self._POOL,
self.devserver,
rjob.record_entry, True,
tests_to_skip,
self._DEPENDENCIES))
self.reimager.clear_reimaged_host_state(self._BUILD)
self.assertTrue(bad_test in tests_to_skip)
def testFailedReimage(self):
"""Should attempt a reimage and record failure."""
canary = FakeJob()
statuses = {canary.hostnames[0]: job_status.Status('FAIL',
canary.hostnames[0])}
self.expect_attempt(canary, statuses)
rjob = self.mox.CreateMock(base_job.base_job)
self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
self.mox.ReplayAll()
self.assertFalse(self.reimager.attempt(self._BUILD, self._POOL,
self.devserver,
rjob.record_entry, True,
[], self._DEPENDENCIES))
self.reimager.clear_reimaged_host_state(self._BUILD)
def testReimageThatNeverHappened(self):
"""Should attempt a reimage and record that it didn't run."""
canary = FakeJob()
statuses = {'hostless': job_status.Status('ABORT', 'big_job_name')}
self.expect_attempt(canary, statuses)
rjob = self.mox.CreateMock(base_job.base_job)
self.mox.ReplayAll()
self.reimager.attempt(self._BUILD, self._POOL, self.devserver,
rjob.record_entry, True, [],
self._DEPENDENCIES)
self.reimager.clear_reimaged_host_state(self._BUILD)
def testReimageThatRaised(self):
"""Should attempt a reimage that raises an exception and record that."""
canary = FakeJob()
ex_message = 'Oh no!'
statuses = {'hostless': job_status.Status('ABORT', 'fake_job')}
self.expect_attempt(canary, statuses=statuses, ex=Exception(ex_message))
rjob = self.mox.CreateMock(base_job.base_job)
rjob.record_entry(StatusContains.CreateFromStrings('START'))
rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
reason=ex_message))
rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
self.mox.ReplayAll()
self.reimager.attempt(self._BUILD, self._POOL, self.devserver,
rjob.record_entry, True, [], self._DEPENDENCIES)
self.reimager.clear_reimaged_host_state(self._BUILD)
def testSuccessfulReimageThatCouldNotScheduleRightAway(self):
"""Should attempt reimage, ignoring host availability; record success.
"""
canary = FakeJob()
statuses = {canary.hostnames[0]: job_status.Status('GOOD',
canary.hostnames[0])}
self.expect_attempt(canary, statuses, check_hosts=False)
rjob = self.mox.CreateMock(base_job.base_job)
self.reimager._clear_build_state(mox.StrContains(canary.hostnames[0]))
self.mox.ReplayAll()
self.assertTrue(self.reimager.attempt(self._BUILD, self._POOL,
self.devserver,
rjob.record_entry, False,
[], self._DEPENDENCIES))
self.reimager.clear_reimaged_host_state(self._BUILD)
def testReimageThatCouldNotSchedule(self):
"""Should attempt a reimage that can't be scheduled."""
self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
self.mox.StubOutWithMock(self.reimager, '_gather_hosts_from_host_specs')
self.mox.StubOutWithMock(self.reimager, '_choose_hosts')
alarm_string = 'alarm!'
self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
self.reimager._gather_hosts_from_host_specs(
mox.IgnoreArg()).AndReturn({})
self.reimager._choose_hosts(
mox.IgnoreArg(),
mox.IgnoreArg(),
True).AndRaise(error.InadequateHostsException(alarm_string))
rjob = self.mox.CreateMock(base_job.base_job)
rjob.record_entry(StatusContains.CreateFromStrings('START'))
rjob.record_entry(StatusContains.CreateFromStrings('WARN',
reason=alarm_string))
rjob.record_entry(StatusContains.CreateFromStrings('END WARN'))
self.mox.ReplayAll()
self.reimager.attempt(self._BUILD, self._POOL, self.devserver,
rjob.record_entry, True, [], self._DEPENDENCIES)
self.reimager.clear_reimaged_host_state(self._BUILD)
def testReimageWithNoAvailableHosts(self):
"""Should attempt a reimage while all hosts are dead."""
self.mox.StubOutWithMock(self.reimager, '_ensure_version_label')
self.reimager._ensure_version_label(mox.StrContains(self._BUILD))
self.mox.StubOutWithMock(self.reimager, '_gather_hosts_from_host_specs')
self.reimager._gather_hosts_from_host_specs(
mox.IgnoreArg()).AndReturn({})
self.mox.StubOutWithMock(self.reimager, '_choose_hosts')
alarm_string = 'alarm!'
self.reimager._choose_hosts(
mox.IgnoreArg(),
mox.IgnoreArg(),
True).AndRaise(error.NoHostsException(alarm_string))
rjob = self.mox.CreateMock(base_job.base_job)
rjob.record_entry(StatusContains.CreateFromStrings('START'))
rjob.record_entry(StatusContains.CreateFromStrings('ERROR',
reason=alarm_string))
rjob.record_entry(StatusContains.CreateFromStrings('END ERROR'))
self.mox.ReplayAll()
self.reimager.attempt(self._BUILD, self._POOL, self.devserver,
rjob.record_entry, True, [], self._DEPENDENCIES)
self.reimager.clear_reimaged_host_state(self._BUILD)