2 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Unittests for generic stages."""
15 sys.path.insert(0, os.path.abspath('%s/../../..' % os.path.dirname(__file__)))
16 from chromite.cbuildbot import commands
17 from chromite.cbuildbot import cbuildbot_config as config
18 from chromite.cbuildbot import failures_lib
19 from chromite.cbuildbot import results_lib
20 from chromite.cbuildbot import cbuildbot_run
21 from chromite.cbuildbot import portage_utilities
22 from chromite.cbuildbot.stages import generic_stages
23 from chromite.lib import cros_build_lib
24 from chromite.lib import cros_build_lib_unittest
25 from chromite.lib import cros_test_lib
26 from chromite.lib import osutils
27 from chromite.lib import parallel
28 from chromite.lib import partial_mock
29 from chromite.scripts import cbuildbot
31 # TODO(build): Finish test wrapper (http://crosbug.com/37517).
32 # Until then, this has to be after the chromite imports.
36 DEFAULT_BUILD_NUMBER = 1234321
39 # The inheritence order ensures the patchers are stopped before
40 # cleaning up the temporary directories.
41 # pylint: disable=E1111,E1120,W0212,R0901,R0904
42 class StageTest(cros_test_lib.MockOutputTestCase,
43 cros_test_lib.MoxTempDirTestCase):
44 """Test running a single stage in isolation."""
46 TARGET_MANIFEST_BRANCH = 'ooga_booga'
47 BUILDROOT = 'buildroot'
49 # Subclass should override this to default to a different build config
51 BOT_ID = 'x86-generic-paladin'
53 # Subclasses can override this. If non-None, value is inserted into
54 # self._run.attrs.release_tag.
58 # Prepare a fake build root in self.tempdir, save at self.build_root.
59 self.build_root = os.path.join(self.tempdir, self.BUILDROOT)
60 osutils.SafeMakedirs(os.path.join(self.build_root, '.repo'))
62 self._manager = parallel.Manager()
63 self._manager.__enter__()
65 # These are here to make pylint happy. Values filled in by _Prepare.
67 self._current_board = None
71 def _Prepare(self, bot_id=None, extra_config=None, cmd_args=None,
73 """Prepare a BuilderRun at self._run for this test.
75 This method must allow being called more than once. Subclasses can
76 override this method, but those subclass methods should also call this one.
78 The idea is that all test preparation that falls out from the choice of
79 build config and cbuildbot options should go in _Prepare.
81 This will populate the following attributes on self:
82 run: A BuilderRun object.
83 bot_id: The bot id (name) that was used from config.config.
84 self._boards: Same as self._run.config.boards. TODO(mtennant): remove.
85 self._current_board: First board in list, if there is one.
88 bot_id: Name of build config to use, defaults to self.BOT_ID.
89 extra_config: Dict used to add to the build config for the given
90 bot_id. Example: {'push_image': True}.
91 cmd_args: List to override the default cbuildbot command args.
92 extra_cmd_args: List to add to default cbuildbot command args. This
93 is a good way to adjust an options value for your test.
94 Example: ['branch-name', 'some-branch-name'] will effectively cause
95 self._run.options.branch_name to be set to 'some-branch-name'.
97 # Use cbuildbot parser to create options object and populate default values.
98 parser = cbuildbot._CreateParser()
100 # Fill in default command args.
102 '-r', self.build_root, '--buildbot', '--noprebuilts',
103 '--buildnumber', str(DEFAULT_BUILD_NUMBER),
104 '--branch', self.TARGET_MANIFEST_BRANCH,
107 cmd_args += extra_cmd_args
108 (options, args) = parser.parse_args(cmd_args)
110 # The bot_id can either be specified as arg to _Prepare method or in the
111 # cmd_args (as cbuildbot normally accepts it from command line).
113 self._bot_id = args[0]
115 # This means bot_id was specified as _Prepare arg and in cmd_args.
116 # Make sure they are the same.
117 self.assertEquals(self._bot_id, bot_id)
119 self._bot_id = bot_id or self.BOT_ID
120 args = [self._bot_id]
121 cbuildbot._FinishParsing(options, args)
123 # Populate build_config corresponding to self._bot_id.
124 build_config = copy.deepcopy(config.config[self._bot_id])
125 build_config['manifest_repo_url'] = 'fake_url'
127 build_config.update(extra_config)
128 if options.remote_trybot:
129 build_config = config.OverrideConfigForTrybot(build_config, options)
131 self._boards = build_config['boards']
132 self._current_board = self._boards[0] if self._boards else None
134 # Some preliminary sanity checks.
135 self.assertEquals(options.buildroot, self.build_root)
137 # Construct a real BuilderRun using options and build_config.
138 self._run = cbuildbot_run.BuilderRun(options, build_config, self._manager)
140 if self.RELEASE_TAG is not None:
141 self._run.attrs.release_tag = self.RELEASE_TAG
143 portage_utilities._OVERLAY_LIST_CMD = '/bin/true'
146 # Mimic exiting with statement for self._manager.
147 self._manager.__exit__(None, None, None)
149 def AutoPatch(self, to_patch):
150 """Patch a list of objects with autospec=True.
153 to_patch: A list of tuples in the form (target, attr) to patch. Will be
154 directly passed to mock.patch.object.
156 for item in to_patch:
157 self.PatchObject(*item, autospec=True)
159 def GetHWTestSuite(self):
160 """Get the HW test suite for the current bot."""
161 hw_tests = self._run.config['hw_tests']
163 # TODO(milleral): Add HWTests back to lumpy-chrome-perf.
164 raise unittest.SkipTest('Missing HWTest for %s' % (self._bot_id,))
169 class AbstractStageTest(StageTest):
170 """Base class for tests that test a particular build stage.
172 Abstract base class that sets up the build config and options with some
173 default values for testing BuilderStage and its derivatives.
176 def ConstructStage(self):
177 """Returns an instance of the stage to be tested.
178 Implement in subclasses.
180 raise NotImplementedError(self, "ConstructStage: Implement in your test")
183 """Creates and runs an instance of the stage to be tested.
184 Requires ConstructStage() to be implemented.
187 NotImplementedError: ConstructStage() was not implemented.
190 # Stage construction is usually done as late as possible because the tests
191 # set up the build configuration and options used in constructing the stage.
192 results_lib.Results.Clear()
193 stage = self.ConstructStage()
195 self.assertTrue(results_lib.Results.BuildSucceededSoFar())
198 def patch(*args, **kwargs):
199 """Convenience wrapper for mock.patch.object.
201 Sets autospec=True by default.
203 kwargs.setdefault('autospec', True)
204 return mock.patch.object(*args, **kwargs)
207 @contextlib.contextmanager
209 """Context manager for a list of patch objects."""
210 with cros_build_lib.ContextManagerStack() as stack:
212 stack.Add(lambda: arg)
216 class BuilderStageTest(AbstractStageTest):
217 """Tests for BuilderStage class."""
222 def ConstructStage(self):
223 return generic_stages.BuilderStage(self._run)
225 def testGetPortageEnvVar(self):
226 """Basic test case for _GetPortageEnvVar function."""
227 self.mox.StubOutWithMock(cros_build_lib, 'RunCommand')
229 obj = cros_test_lib.EasyAttr(output='RESULT\n')
230 cros_build_lib.RunCommand(mox.And(mox.IsA(list), mox.In(envvar)),
231 cwd='%s/src/scripts' % self.build_root,
232 redirect_stdout=True, enter_chroot=True,
233 error_code_ok=True).AndReturn(obj)
236 stage = self.ConstructStage()
237 board = self._current_board
238 result = stage._GetPortageEnvVar(envvar, board)
241 self.assertEqual(result, 'RESULT')
243 def testStageNamePrefixSmoke(self):
244 """Basic test for the StageNamePrefix() function."""
245 stage = self.ConstructStage()
246 self.assertEqual(stage.StageNamePrefix(), 'Builder')
248 def testGetStageNamesSmoke(self):
249 """Basic test for the GetStageNames() function."""
250 stage = self.ConstructStage()
251 self.assertEqual(stage.GetStageNames(), ['Builder'])
253 def testConstructDashboardURLSmoke(self):
254 """Basic test for the ConstructDashboardURL() function."""
255 stage = self.ConstructStage()
257 exp_url = ('http://build.chromium.org/p/chromiumos/builders/'
258 'x86-generic-paladin/builds/%s' % DEFAULT_BUILD_NUMBER)
259 self.assertEqual(stage.ConstructDashboardURL(), exp_url)
261 stage_name = 'Archive'
262 exp_url = '%s/steps/%s/logs/stdio' % (exp_url, stage_name)
263 self.assertEqual(stage.ConstructDashboardURL(stage=stage_name), exp_url)
265 def test_ExtractOverlaysSmoke(self):
266 """Basic test for the _ExtractOverlays() function."""
267 stage = self.ConstructStage()
268 self.assertEqual(stage._ExtractOverlays(), ([], []))
270 def test_PrintSmoke(self):
271 """Basic test for the _Print() function."""
272 stage = self.ConstructStage()
273 with self.OutputCapturer():
274 stage._Print('hi there')
275 self.AssertOutputContainsLine('hi there', check_stderr=True)
277 def test_PrintLoudlySmoke(self):
278 """Basic test for the _PrintLoudly() function."""
279 stage = self.ConstructStage()
280 with self.OutputCapturer():
281 stage._PrintLoudly('hi there')
282 self.AssertOutputContainsLine(r'\*{10}', check_stderr=True)
283 self.AssertOutputContainsLine('hi there', check_stderr=True)
285 def testRunSmoke(self):
286 """Basic passing test for the Run() function."""
287 stage = self.ConstructStage()
288 with self.OutputCapturer():
291 def _RunCapture(self, stage):
292 """Helper method to run Run() with captured output."""
293 output = self.OutputCapturer()
294 output.StartCapturing()
298 output.StopCapturing()
300 def testRunException(self):
301 """Verify stage exceptions are handled."""
302 class TestError(Exception):
303 """Unique test exception"""
305 perform_mock = self.PatchObject(generic_stages.BuilderStage, 'PerformStage')
306 perform_mock.side_effect = TestError('fail!')
308 stage = self.ConstructStage()
309 results_lib.Results.Clear()
310 self.assertRaises(failures_lib.StepFailure, self._RunCapture, stage)
312 results = results_lib.Results.Get()[0]
313 self.assertTrue(isinstance(results.result, TestError))
314 self.assertEqual(str(results.result), 'fail!')
316 def testHandleExceptionException(self):
317 """Verify exceptions in HandleException handlers are themselves handled."""
318 class TestError(Exception):
319 """Unique test exception"""
321 class BadStage(generic_stages.BuilderStage):
322 """Stage that throws an exception when PerformStage is called."""
324 handled_exceptions = []
326 def PerformStage(self):
327 raise TestError('first fail')
329 def _HandleStageException(self, exc_info):
330 self.handled_exceptions.append(str(exc_info[1]))
331 raise TestError('nested')
333 stage = BadStage(self._run)
334 results_lib.Results.Clear()
335 self.assertRaises(failures_lib.StepFailure, self._RunCapture, stage)
337 # Verify the results tracked the original exception.
338 results = results_lib.Results.Get()[0]
339 self.assertTrue(isinstance(results.result, TestError))
340 self.assertEqual(str(results.result), 'first fail')
342 self.assertEqual(stage.handled_exceptions, ['first fail'])
345 class BoardSpecificBuilderStageTest(cros_test_lib.TestCase):
346 """Tests option/config settings on board-specific stages."""
348 # TODO (yjhong): Fix this test.
349 # def testCheckOptions(self):
350 # """Makes sure options/config settings are setup correctly."""
351 # parser = cbuildbot._CreateParser()
352 # (options, _) = parser.parse_args([])
354 # for attr in dir(stages):
355 # obj = eval('stages.' + attr)
356 # if not hasattr(obj, '__base__'):
358 # if not obj.__base__ is stages.BoardSpecificBuilderStage:
360 # if obj.option_name:
361 # self.assertTrue(getattr(options, obj.option_name))
362 # if obj.config_name:
363 # if not obj.config_name in config._settings:
364 # self.fail(('cbuildbot_stages.%s.config_name "%s" is missing from '
365 # 'cbuildbot_config._settings') % (attr, obj.config_name))
367 # pylint: disable=W0223
368 class RunCommandAbstractStageTest(AbstractStageTest,
369 cros_build_lib_unittest.RunCommandTestCase):
370 """Base test class for testing a stage and mocking RunCommand."""
372 FULL_BOT_ID = 'x86-generic-full'
373 BIN_BOT_ID = 'x86-generic-paladin'
375 def _Prepare(self, bot_id, **kwargs):
376 super(RunCommandAbstractStageTest, self)._Prepare(bot_id, **kwargs)
378 def _PrepareFull(self, **kwargs):
379 self._Prepare(self.FULL_BOT_ID, **kwargs)
381 def _PrepareBin(self, **kwargs):
382 self._Prepare(self.BIN_BOT_ID, **kwargs)
384 def _Run(self, dir_exists):
385 """Helper for running the build."""
386 with patch(os.path, 'isdir', return_value=dir_exists):
390 class ArchivingStageMixinMock(partial_mock.PartialMock):
391 """Partial mock for ArchivingStageMixin."""
393 TARGET = 'chromite.cbuildbot.stages.generic_stages.ArchivingStageMixin'
394 ATTRS = ('UploadArtifact',)
396 def UploadArtifact(self, *args, **kwargs):
397 with patch(commands, 'ArchiveFile', return_value='foo.txt'):
398 with patch(commands, 'UploadArchivedFile'):
399 self.backup['UploadArtifact'](*args, **kwargs)
403 if __name__ == '__main__':