Allow lab status to block based on a build regex.
Previously, the lab status could contain a string to block testing
based on the board. With this change, the status can block any
build that matches a regex in the status. This allows closing the
lab to either a build or a milestone. It also allows closing the
lab for other more specific conditions, for anyone desperate enough
to want to.
BUG=chromium:220934
TEST=unit tests
Change-Id: I8a44f5e9be504415a7c5bd73c714c778bf684d2c
Reviewed-on: https://chromium-review.googlesource.com/179544
Tested-by: Richard Barnette <jrbarnette@chromium.org>
Reviewed-by: Alex Miller <milleral@chromium.org>
Commit-Queue: Richard Barnette <jrbarnette@chromium.org>
diff --git a/server/lab_status_unittest.py b/server/lab_status_unittest.py
index fad4285..1299a66 100644
--- a/server/lab_status_unittest.py
+++ b/server/lab_status_unittest.py
@@ -13,6 +13,8 @@
from autotest_lib.client.common_lib import global_config
from autotest_lib.server import site_utils
+_DEADBUILD = 'deadboard-release/R33-4966.0.0'
+_LIVEBUILD = 'liveboard-release/R32-4920.14.0'
_OPEN_STATUS_VALUES = [
'''
@@ -44,6 +46,16 @@
"general_state": "open"
}
''',
+
+ '''
+ {
+ "username": "fizzbin@google.com",
+ "date": "2013-11-16 00:25:23.511208",
+ "message": "Lab is up despite R33-4966.0.0",
+ "can_commit_freely": true,
+ "general_state": "open"
+ }
+ ''',
]
_CLOSED_STATUS_VALUES = [
@@ -61,19 +73,19 @@
{
"username": "fizzbin@google.com",
"date": "2013-11-16 00:25:23.511208",
- "message": "Lab is down even for [deadboard]",
+ "message": "Lab is down even for [liveboard-release/R32-4920.14.0]",
"can_commit_freely": false,
"general_state": "closed"
}
''',
]
-_DEADBOARD_STATUS_VALUES = [
+_DEADBUILD_STATUS_VALUES = [
'''
{
"username": "fizzbin@google.com",
"date": "2013-11-16 00:25:23.511208",
- "message": "Lab is up except for [deadboard]",
+ "message": "Lab is up except for [deadboard-]",
"can_commit_freely": false,
"general_state": "open"
}
@@ -83,7 +95,7 @@
{
"username": "fizzbin@google.com",
"date": "2013-11-16 00:25:23.511208",
- "message": "liveboard is good, but [deadboard] is bad",
+ "message": "Lab is up except for [R33-]",
"can_commit_freely": false,
"general_state": "open"
}
@@ -93,7 +105,7 @@
{
"username": "fizzbin@google.com",
"date": "2013-11-16 00:25:23.511208",
- "message": "Lab is up [deadboard otherboard]",
+ "message": "Lab is up except for [deadboard-.*/R33-]",
"can_commit_freely": false,
"general_state": "open"
}
@@ -103,7 +115,7 @@
{
"username": "fizzbin@google.com",
"date": "2013-11-16 00:25:23.511208",
- "message": "Lab is up [otherboard deadboard]",
+ "message": "Lab is up except for [ deadboard-]",
"can_commit_freely": false,
"general_state": "open"
}
@@ -113,7 +125,47 @@
{
"username": "fizzbin@google.com",
"date": "2013-11-16 00:25:23.511208",
- "message": "Lab is up [first deadboard last]",
+ "message": "Lab is up except for [deadboard- ]",
+ "can_commit_freely": false,
+ "general_state": "open"
+ }
+ ''',
+
+ '''
+ {
+ "username": "fizzbin@google.com",
+ "date": "2013-11-16 00:25:23.511208",
+ "message": "Lab is up [first R33- last]",
+ "can_commit_freely": false,
+ "general_state": "open"
+ }
+ ''',
+
+ '''
+ {
+ "username": "fizzbin@google.com",
+ "date": "2013-11-16 00:25:23.511208",
+ "message": "liveboard is good, but [deadboard-] is bad",
+ "can_commit_freely": false,
+ "general_state": "open"
+ }
+ ''',
+
+ '''
+ {
+ "username": "fizzbin@google.com",
+ "date": "2013-11-16 00:25:23.511208",
+ "message": "Lab is up [deadboard- otherboard-]",
+ "can_commit_freely": false,
+ "general_state": "open"
+ }
+ ''',
+
+ '''
+ {
+ "username": "fizzbin@google.com",
+ "date": "2013-11-16 00:25:23.511208",
+ "message": "Lab is up [otherboard- deadboard-]",
"can_commit_freely": false,
"general_state": "open"
}
@@ -249,10 +301,10 @@
1. Lab is up. All calls to _decode_lab_status() will
succeed without raising an exception.
2. Lab is down. All calls to _decode_lab_status() will
- fail with LabIsDownException.
- 3. Board disabled. Calls to _decode_lab_status() will
- succeed, except that board 'deadboard' will raise
- BoardIsDisabledException.
+ fail with TestLabException.
+ 3. Build disabled. Calls to _decode_lab_status() will
+ succeed, except that board `_DEADBUILD` will raise
+ TestLabException.
"""
@@ -265,42 +317,37 @@
@param lab_status JSON value describing lab status.
"""
- site_utils._decode_lab_status(lab_status, None)
- site_utils._decode_lab_status(lab_status, 'liveboard')
- site_utils._decode_lab_status(lab_status, 'deadboard')
+ site_utils._decode_lab_status(lab_status, _LIVEBUILD)
+ site_utils._decode_lab_status(lab_status, _DEADBUILD)
def _assert_lab_closed(self, lab_status):
"""Test that closed status values are handled properly.
- Test that _decode_lab_status() raises LabIsDownException
+ Test that _decode_lab_status() raises TestLabException
when the lab status is down.
@param lab_status JSON value describing lab status.
"""
- with self.assertRaises(site_utils.LabIsDownException):
- site_utils._decode_lab_status(lab_status, None)
- with self.assertRaises(site_utils.LabIsDownException):
- site_utils._decode_lab_status(lab_status, 'liveboard')
- with self.assertRaises(site_utils.LabIsDownException):
- site_utils._decode_lab_status(lab_status, 'deadboard')
+ with self.assertRaises(site_utils.TestLabException):
+ site_utils._decode_lab_status(lab_status, _LIVEBUILD)
+ with self.assertRaises(site_utils.TestLabException):
+ site_utils._decode_lab_status(lab_status, _DEADBUILD)
- def _assert_lab_deadboard(self, lab_status):
- """Test that disabled boards are handled properly.
+ def _assert_lab_deadbuild(self, lab_status):
+ """Test that disabled builds are handled properly.
- Test that _decode_lab_status() raises
- BoardIsDisabledException for board 'deadboard' and
- succeeds otherwise.
+ Test that _decode_lab_status() raises TestLabException
+ for build `_DEADBUILD` and succeeds otherwise.
@param lab_status JSON value describing lab status.
"""
- site_utils._decode_lab_status(lab_status, None)
- site_utils._decode_lab_status(lab_status, 'liveboard')
- with self.assertRaises(site_utils.BoardIsDisabledException):
- site_utils._decode_lab_status(lab_status, 'deadboard')
+ site_utils._decode_lab_status(lab_status, _LIVEBUILD)
+ with self.assertRaises(site_utils.TestLabException):
+ site_utils._decode_lab_status(lab_status, _DEADBUILD)
def _assert_lab_status(self, test_values, checker):
@@ -333,10 +380,10 @@
self._assert_lab_closed)
- def test_dead_board(self):
- """Test that disabled boards are handled correctly."""
- self._assert_lab_status(_DEADBOARD_STATUS_VALUES,
- self._assert_lab_deadboard)
+ def test_dead_build(self):
+ """Test that disabled builds are handled correctly."""
+ self._assert_lab_status(_DEADBUILD_STATUS_VALUES,
+ self._assert_lab_deadbuild)
class CheckStatusTest(mox.MoxTestBase):
@@ -389,100 +436,51 @@
site_utils._get_lab_status(_FAKE_URL).AndReturn(json_value)
- def _try_check_no_board(self):
- """Test calling check_lab_status() with no board."""
+ def _try_check_status(self, build):
+ """Test calling check_lab_status() with `build`."""
try:
self.mox.ReplayAll()
- site_utils.check_lab_status()
+ site_utils.check_lab_status(build)
finally:
self.mox.VerifyAll()
- def _try_check_dead_board(self):
- """Test calling check_lab_status() with 'deadboard'."""
- try:
- self.mox.ReplayAll()
- site_utils.check_lab_status('deadboard')
- finally:
- self.mox.VerifyAll()
-
-
- def _try_check_live_board(self):
- """Test calling check_lab_status() with 'liveboard'."""
- try:
- self.mox.ReplayAll()
- site_utils.check_lab_status('liveboard')
- finally:
- self.mox.VerifyAll()
-
-
- def test_non_cautotest_no_board(self):
- """Test a call with no board when the host isn't cautotest."""
+ def test_non_cautotest(self):
+ """Test a call with a build when the host isn't cautotest."""
self._setup_not_cautotest()
- self._try_check_no_board()
+ self._try_check_status(_LIVEBUILD)
- def test_non_cautotest_with_board(self):
- """Test a call with a board when the host isn't cautotest."""
- self._setup_not_cautotest()
- self._try_check_live_board()
-
-
- def test_no_status_no_board(self):
- """Test without a board when `_get_lab_status()` returns `None`."""
+ def test_no_lab_status(self):
+ """Test with a build when `_get_lab_status()` returns `None`."""
self._setup_no_status()
- self._try_check_no_board()
+ self._try_check_status(_LIVEBUILD)
- def test_no_lab_status_with_board(self):
- """Test with a board when `_get_lab_status()` returns `None`."""
- self._setup_no_status()
- self._try_check_live_board()
-
-
- def test_lab_up_no_board(self):
- """Test lab open with no board specified."""
+ def test_lab_up_live_build(self):
+ """Test lab open with a build specified."""
self._setup_lab_status(_OPEN_STATUS_VALUES[0])
- self._try_check_no_board()
+ self._try_check_status(_LIVEBUILD)
- def test_lab_up_live_board(self):
- """Test lab open with a board specified."""
- self._setup_lab_status(_OPEN_STATUS_VALUES[0])
- self._try_check_live_board()
-
-
- def test_lab_down_no_board(self):
- """Test lab closed with no board specified."""
+ def test_lab_down_live_build(self):
+ """Test lab closed with a build specified."""
self._setup_lab_status(_CLOSED_STATUS_VALUES[0])
- with self.assertRaises(site_utils.LabIsDownException):
- self._try_check_no_board()
+ with self.assertRaises(site_utils.TestLabException):
+ self._try_check_status(_LIVEBUILD)
- def test_lab_down_live_board(self):
- """Test lab closed with a board specified."""
- self._setup_lab_status(_CLOSED_STATUS_VALUES[0])
- with self.assertRaises(site_utils.LabIsDownException):
- self._try_check_live_board()
+ def test_build_disabled_live_build(self):
+ """Test build disabled with a live build specified."""
+ self._setup_lab_status(_DEADBUILD_STATUS_VALUES[0])
+ self._try_check_status(_LIVEBUILD)
- def test_board_disabled_no_board(self):
- """Test board disabled with no board specified."""
- self._setup_lab_status(_DEADBOARD_STATUS_VALUES[0])
- self._try_check_no_board()
-
-
- def test_board_disabled_live_board(self):
- """Test board disabled with a live board specified."""
- self._setup_lab_status(_DEADBOARD_STATUS_VALUES[0])
- self._try_check_live_board()
-
-
- def test_board_disabled_dead_board(self):
- """Test board disabled with the disabled board specified."""
- self._setup_lab_status(_DEADBOARD_STATUS_VALUES[0])
- with self.assertRaises(site_utils.BoardIsDisabledException):
- self._try_check_dead_board()
+ def test_build_disabled_dead_build(self):
+ """Test build disabled with the disabled build specified."""
+ self._setup_lab_status(_DEADBUILD_STATUS_VALUES[0])
+ with self.assertRaises(site_utils.TestLabException):
+ self._try_check_status(_DEADBUILD)
if __name__ == '__main__':
diff --git a/server/site_utils.py b/server/site_utils.py
index daa0a6b..d27aff3 100644
--- a/server/site_utils.py
+++ b/server/site_utils.py
@@ -25,13 +25,8 @@
LAB_GOOD_STATES = ('open', 'throttled')
-class LabIsDownException(Exception):
- """Raised when the Lab is Down"""
- pass
-
-
-class BoardIsDisabledException(Exception):
- """Raised when a certain board is disabled in the Lab"""
+class TestLabException(Exception):
+ """Exception raised when the Test Lab blocks a test or suite."""
pass
@@ -178,50 +173,48 @@
return None
-def _decode_lab_status(lab_status, board):
+def _decode_lab_status(lab_status, build):
"""Decode lab status, and report exceptions as needed.
- Takes a deserialized JSON object from the lab status page, and
- interprets it to determine the actual lab status. Raises
+ Take a deserialized JSON object from the lab status page, and
+ interpret it to determine the actual lab status. Raise
exceptions as required to report when the lab is down.
- @param board: board name that we want to check the status of.
+ @param build: build name that we want to check the status of.
- @raises LabIsDownException if the lab is not up.
- @raises BoardIsDisabledException if the desired board is currently
- disabled.
+ @raises TestLabException Raised if a request to test for the given
+ status and build should be blocked.
"""
# First check if the lab is up.
if not lab_status['general_state'] in LAB_GOOD_STATES:
- raise LabIsDownException('Chromium OS Lab is currently not up: '
- '%s.' % lab_status['message'])
+ raise TestLabException('Chromium OS Test Lab is closed: '
+ '%s.' % lab_status['message'])
- # Check if the board we wish to use is disabled.
+ # Check if the build we wish to use is disabled.
# Lab messages should be in the format of:
- # Lab is 'status' [boards not to be ran] (comment). Example:
- # Lab is Open [stumpy, kiev, x86-alex] (power_resume rtc causing duts to go
- # down)
- boards_are_disabled = re.search('\[(.*)\]', lab_status['message'])
- if board and boards_are_disabled:
- if board in boards_are_disabled.group(1):
- raise BoardIsDisabledException('Chromium OS Lab is '
- 'currently not allowing suites to be scheduled on board '
- '%s: %s' % (board, lab_status['message']))
+ # Lab is 'status' [regex ...] (comment)
+ # If the build name matches any regex, it will be blocked.
+ build_exceptions = re.search('\[(.*)\]', lab_status['message'])
+ if not build_exceptions:
+ return
+ for build_pattern in build_exceptions.group(1).split():
+ if re.search(build_pattern, build):
+ raise TestLabException('Chromium OS Test Lab is closed: '
+ '%s matches %s.' % (
+ build, build_pattern))
return
-def check_lab_status(board=None):
- """Check if the lab status allows us to schedule suites.
+def check_lab_status(build):
+ """Check if the lab status allows us to schedule for a build.
- Also checks if the lab is disabled for that particular board, and if so
- will raise an error to prevent new suites from being scheduled for that
- board.
+ Checks if the lab is down, or if testing for the requested build
+ should be blocked.
- @param board: board name that we want to check the status of.
+ @param build: Name of the build to be scheduled for testing.
- @raises LabIsDownException if the lab is not up.
- @raises BoardIsDisabledException if the desired board is currently
- disabled.
+ @raises TestLabException Raised if a request to test for the given
+ status and build should be blocked.
"""
# Ensure we are trying to schedule on the actual lab.
@@ -238,4 +231,4 @@
# We go ahead and say the lab is open if we can't get the status.
logging.warn('Could not get a status from %s', status_url)
return
- _decode_lab_status(json_status, board)
+ _decode_lab_status(json_status, build)
diff --git a/site_utils/run_suite.py b/site_utils/run_suite.py
index 689b45d..6d2a67a 100755
--- a/site_utils/run_suite.py
+++ b/site_utils/run_suite.py
@@ -597,8 +597,8 @@
try:
if not options.bypass_labstatus:
- utils.check_lab_status(options.board)
- except (utils.LabIsDownException, utils.BoardIsDisabledException) as e:
+ utils.check_lab_status(options.build)
+ except utils.TestLabException as e:
logging.warning('Error Message: %s', e)
return RETURN_CODES.WARNING
diff --git a/site_utils/suite_scheduler/deduping_scheduler.py b/site_utils/suite_scheduler/deduping_scheduler.py
index c27faaf..1edafd4 100644
--- a/site_utils/suite_scheduler/deduping_scheduler.py
+++ b/site_utils/suite_scheduler/deduping_scheduler.py
@@ -49,8 +49,8 @@
def _ShouldScheduleSuite(self, suite, board, build):
"""Return True if |suite| has not yet been run for |build| on |board|.
- True if |suite| has not been run for |build| on |board|.
- False if it has been.
+ True if |suite| has not been run for |build| on |board|, and
+ the lab is open for this particular request. False otherwise.
@param suite: the name of the suite to run, e.g. 'bvt'
@param board: the board to run the suite on, e.g. x86-alex
@@ -60,6 +60,12 @@
@raise DedupException if the AFE raises while searching for jobs.
"""
try:
+ site_utils.check_lab_status(build)
+ except site_utils.TestLabException as ex:
+ logging.debug('Skipping suite %s, board %s, build %s: %s',
+ suite, board, build, str(ex))
+ return False
+ try:
return not self._afe.get_jobs(name__startswith=build,
name__endswith='control.'+suite)
except Exception as e:
diff --git a/site_utils/suite_scheduler/deduping_scheduler_unittest.py b/site_utils/suite_scheduler/deduping_scheduler_unittest.py
index a92e3ee..9002547 100644
--- a/site_utils/suite_scheduler/deduping_scheduler_unittest.py
+++ b/site_utils/suite_scheduler/deduping_scheduler_unittest.py
@@ -42,10 +42,30 @@
super(DedupingSchedulerTest, self).setUp()
self.afe = self.mox.CreateMock(frontend.AFE)
self.scheduler = deduping_scheduler.DedupingScheduler(afe=self.afe)
+ self.mox.StubOutWithMock(site_utils, 'check_lab_status')
+
+
+ def _SetupLabStatus(self, build, message=None):
+ """Set up to mock one call to `site_utils.check_lab_status()`.
+
+ @param build The build to expect to be passed to
+ `check_lab_status()`.
+ @param message `None` if the mocked call should return that
+ the lab status is up. Otherwise, a string for
+ the exception message.
+
+ """
+ if message is None:
+ site_utils.check_lab_status(build)
+ else:
+ site_utils.check_lab_status(build).AndRaise(
+ site_utils.TestLabException(message))
def testScheduleSuite(self):
"""Test a successful de-dup and suite schedule."""
+ # Lab is UP!
+ self._SetupLabStatus(self._BUILD)
# A similar suite has not already been scheduled.
self.afe.get_jobs(name__startswith=self._BUILD,
name__endswith='control.'+self._SUITE).AndReturn([])
@@ -72,6 +92,8 @@
def testShouldNotScheduleSuite(self):
"""Test a successful de-dup and avoiding scheduling the suite."""
+ # Lab is UP!
+ self._SetupLabStatus(self._BUILD)
# A similar suite has already been scheduled.
self.afe.get_jobs(
name__startswith=self._BUILD,
@@ -86,6 +108,20 @@
self._TIMEOUT))
+ def testShouldNotScheduleSuiteLabClosed(self):
+ """Test that we don't schedule when the lab is closed."""
+ # Lab is down. :-(
+ self._SetupLabStatus(self._BUILD, 'Lab closed due to sheep.')
+ self.mox.ReplayAll()
+ self.assertFalse(self.scheduler.ScheduleSuite(self._SUITE,
+ self._BOARD,
+ self._BUILD,
+ self._POOL,
+ None,
+ self._PRIORITY,
+ self._TIMEOUT))
+
+
def testForceScheduleSuite(self):
"""Test a successful de-dup, but force scheduling the suite."""
# Expect an attempt to schedule; allow it to succeed.
@@ -112,6 +148,8 @@
def testShouldScheduleSuiteExplodes(self):
"""Test a failure to de-dup."""
+ # Lab is UP!
+ self._SetupLabStatus(self._BUILD)
# Barf while checking for similar suites.
self.afe.get_jobs(
name__startswith=self._BUILD,
@@ -130,6 +168,8 @@
def testScheduleFail(self):
"""Test a successful de-dup and failure to schedule the suite."""
+ # Lab is UP!
+ self._SetupLabStatus(self._BUILD)
# A similar suite has not already been scheduled.
self.afe.get_jobs(name__startswith=self._BUILD,
name__endswith='control.'+self._SUITE).AndReturn([])
@@ -158,6 +198,8 @@
def testScheduleExplodes(self):
"""Test a successful de-dup and barf while scheduling the suite."""
+ # Lab is UP!
+ self._SetupLabStatus(self._BUILD)
# A similar suite has not already been scheduled.
self.afe.get_jobs(name__startswith=self._BUILD,
name__endswith='control.'+self._SUITE).AndReturn([])
@@ -191,6 +233,8 @@
self.mox.StubOutWithMock(reporting.Reporter, '_check_tracker')
self.mox.StubOutWithMock(site_utils, 'get_sheriffs')
self.scheduler._file_bug = True
+ # Lab is UP!
+ self._SetupLabStatus(self._BUILD)
# A similar suite has not already been scheduled.
self.afe.get_jobs(name__startswith=self._BUILD,
name__endswith='control.'+self._SUITE).AndReturn([])
diff --git a/site_utils/suite_scheduler/driver.py b/site_utils/suite_scheduler/driver.py
index cda71f5..b43b196 100644
--- a/site_utils/suite_scheduler/driver.py
+++ b/site_utils/suite_scheduler/driver.py
@@ -132,13 +132,6 @@
logging.info('Boards currently in the lab: %r', boards)
for e in self._events.itervalues():
if e.ShouldHandle():
- try:
- utils.check_lab_status()
- except utils.LabIsDownException as ex:
- logging.debug('Skipping event %s, because lab is down '
- 'with message: %s', e.keyword, ex.message)
- continue
-
logging.info('Handling %s event', e.keyword)
for board in boards:
branch_builds = e.GetBranchBuildsForBoard(board)
diff --git a/site_utils/suite_scheduler/driver_unittest.py b/site_utils/suite_scheduler/driver_unittest.py
index f949ceb..c754d1c 100644
--- a/site_utils/suite_scheduler/driver_unittest.py
+++ b/site_utils/suite_scheduler/driver_unittest.py
@@ -15,7 +15,6 @@
import common
from autotest_lib.server import frontend
-from autotest_lib.server import utils
from constants import Labels
@@ -56,7 +55,6 @@
self.mox.StubOutWithMock(timed_event.Nightly, 'CreateFromConfig')
self.mox.StubOutWithMock(timed_event.Weekly, 'CreateFromConfig')
self.mox.StubOutWithMock(build_event.NewBuild, 'CreateFromConfig')
- self.mox.StubOutWithMock(utils, 'check_lab_status')
timed_event.Nightly.CreateFromConfig(
mox.IgnoreArg(), self.mv).AndReturn(mock_nightly)
timed_event.Weekly.CreateFromConfig(
@@ -88,19 +86,6 @@
self.afe.get_labels(name__startswith=prefix).AndReturn(mocks)
- def _ExpectLabStatusQuery(self, lab_up=True, message='No message.'):
- """Expect one call to utils.check_lab_status
- @param lab_up: True if lab shoule be up. False if lab should be down.
- Default: True.
- @param message: String message with which lab should be down, if down.
- """
- if lab_up:
- utils.check_lab_status()
- else:
- utils.check_lab_status().AndRaise(
- utils.LabIsDownException(message))
-
-
def _ExpectHandle(self, event, group):
"""Make event report that it's handle-able, and expect it to be handle.
@@ -176,7 +161,6 @@
events = self._ExpectSetup()
self._ExpectEnumeration()
for event in events:
- self._ExpectLabStatusQuery()
self._ExpectHandle(event, 'events')
self.mox.ReplayAll()
@@ -190,7 +174,6 @@
self._ExpectEnumeration()
for event in events:
if event.keyword == timed_event.Nightly.KEYWORD:
- self._ExpectLabStatusQuery()
self._ExpectHandle(event, 'events')
else:
event.ShouldHandle().InAnyOrder('events').AndReturn(False)
@@ -200,19 +183,6 @@
self.driver.HandleEventsOnce(self.mv)
- def testClosedLab(self):
- """Test that events do not get handled if the lab is closed."""
- events = self._ExpectSetup()
- self._ExpectEnumeration()
- for event in events:
- self._ExpectLabStatusQuery(False, 'Lab closed due to sheep.')
- self._ExpectNoHandle(event, 'events')
- self.mox.ReplayAll()
-
- self.driver.SetUpEventsAndTasks(self.config, self.mv)
- self.driver.HandleEventsOnce(self.mv)
-
-
def testForceOnceForBuild(self):
"""Test that one event being forced is handled correctly."""
events = self._ExpectSetup()