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 build stages."""
8 from __future__ import print_function
13 sys.path.insert(0, os.path.abspath('%s/../../..' % os.path.dirname(__file__)))
14 from chromite.cbuildbot.cbuildbot_unittest import BuilderRunMock
15 from chromite.cbuildbot.stages import artifact_stages
16 from chromite.cbuildbot.stages import generic_stages_unittest
17 from chromite.cbuildbot.stages import release_stages
18 from chromite.cbuildbot import failures_lib
19 from chromite.cbuildbot import results_lib
20 from chromite.lib import cros_test_lib
21 from chromite.lib import timeout_util
23 from chromite.cbuildbot.stages.generic_stages_unittest import patch
25 from chromite.lib.paygen import gspaths
26 from chromite.lib.paygen import paygen_build_lib
28 # TODO(build): Finish test wrapper (http://crosbug.com/37517).
29 # Until then, this has to be after the chromite imports.
32 # pylint: disable=R0901, W0212
33 class PaygenStageTest(generic_stages_unittest.AbstractStageTest):
34 """Test the PaygenStageStage."""
35 BOT_ID = 'x86-mario-release'
39 { "status": { "status": "passed" }, "board": "link",
40 "keyset": "link-mp-v4", "type": "recovery", "channel": "stable" }
43 INSNS_URLS_PER_CHANNEL = {
44 'chan1': ['chan1_uri1', 'chan1_uri2'],
45 'chan2': ['chan2_uri1'],
49 self.StartPatcher(BuilderRunMock())
52 def ConstructStage(self):
53 archive_stage = artifact_stages.ArchiveStage(self._run, self._current_board)
54 return release_stages.PaygenStage(self._run, self._current_board,
57 def testWaitForPushImageSuccess(self):
58 """Test waiting for input from PushImage."""
59 stage = self.ConstructStage()
60 stage.board_runattrs.SetParallel(
61 'instruction_urls_per_channel', self.INSNS_URLS_PER_CHANNEL)
63 self.assertEqual(stage._WaitForPushImage(), self.INSNS_URLS_PER_CHANNEL)
65 def testWaitForPushImageError(self):
66 """Test WaitForPushImageError with an error output from pushimage."""
67 stage = self.ConstructStage()
68 stage.board_runattrs.SetParallel(
69 'instruction_urls_per_channel', None)
71 self.assertRaises(release_stages.MissingInstructionException,
72 stage._WaitForPushImage)
74 def testWaitForSigningResultsSuccess(self):
75 """Test that _WaitForSigningResults works when signing works."""
76 results = ['chan1_uri1.json', 'chan1_uri2.json', 'chan2_uri1.json']
78 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
79 mock_gs_ctx = mock_gs_ctx_init.return_value
80 mock_gs_ctx.Cat.return_value.output = self.SIGNER_RESULT
81 notifier = mock.Mock()
83 stage = self.ConstructStage()
84 stage._WaitForSigningResults(self.INSNS_URLS_PER_CHANNEL, notifier)
86 self.assertEqual(notifier.mock_calls,
90 for result in results:
91 mock_gs_ctx.Cat.assert_any_call(result)
93 def testWaitForSigningResultsSuccessNothingSigned(self):
94 """Test _WaitForSigningResults when there are no signed images."""
95 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
96 mock_gs_ctx = mock_gs_ctx_init.return_value
97 mock_gs_ctx.Cat.return_value.output = self.SIGNER_RESULT
98 notifier = mock.Mock()
100 stage = self.ConstructStage()
101 stage._WaitForSigningResults({}, notifier)
103 self.assertEqual(notifier.mock_calls, [])
104 self.assertEqual(mock_gs_ctx.Cat.mock_calls, [])
106 def testWaitForSigningResultsFailure(self):
107 """Test _WaitForSigningResults when the signers report an error."""
108 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
109 mock_gs_ctx = mock_gs_ctx_init.return_value
110 mock_gs_ctx.Cat.return_value.output = """
111 { "status": { "status": "failed" }, "board": "link",
112 "keyset": "link-mp-v4", "type": "recovery", "channel": "stable" }
114 notifier = mock.Mock()
116 stage = self.ConstructStage()
118 self.assertRaises(release_stages.SignerFailure,
119 stage._WaitForSigningResults,
120 {'chan1': ['chan1_uri1']}, notifier)
122 self.assertEqual(notifier.mock_calls, [])
123 self.assertEqual(mock_gs_ctx.Cat.mock_calls,
124 [mock.call('chan1_uri1.json')])
126 def testWaitForSigningResultsMalformedJson(self):
127 """Test _WaitForSigningResults when invalid Json is received.."""
128 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
129 mock_gs_ctx = mock_gs_ctx_init.return_value
130 mock_gs_ctx.Cat.return_value.output = "{"
131 notifier = mock.Mock()
133 stage = self.ConstructStage()
135 self.assertRaises(release_stages.MalformedResultsException,
136 stage._WaitForSigningResults,
137 self.INSNS_URLS_PER_CHANNEL, notifier)
139 self.assertEqual(notifier.mock_calls, [])
140 self.assertEqual(mock_gs_ctx.Cat.mock_calls,
141 [mock.call('chan1_uri1.json')])
143 def testWaitForSigningResultsTimeout(self):
144 """Test that _WaitForSigningResults reports timeouts correctly."""
145 with patch(release_stages.timeout_util, 'WaitForSuccess') as mock_wait:
146 mock_wait.side_effect = timeout_util.TimeoutError
147 notifier = mock.Mock()
149 stage = self.ConstructStage()
151 self.assertRaises(release_stages.SignerResultsTimeout,
152 stage._WaitForSigningResults,
153 {'chan1': ['chan1_uri1']}, notifier)
155 self.assertEqual(notifier.mock_calls, [])
157 def testCheckForResultsSuccess(self):
158 """Test that _CheckForResults works when signing works."""
159 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
160 mock_gs_ctx = mock_gs_ctx_init.return_value
161 mock_gs_ctx.Cat.return_value.output = self.SIGNER_RESULT
162 notifier = mock.Mock()
164 stage = self.ConstructStage()
166 stage._CheckForResults(mock_gs_ctx,
167 self.INSNS_URLS_PER_CHANNEL,
169 self.assertEqual(notifier.mock_calls,
170 [mock.call('chan1'), mock.call('chan2')])
172 def testCheckForResultsSuccessNoChannels(self):
173 """Test that _CheckForResults works when there is nothing to check for."""
174 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
175 mock_gs_ctx = mock_gs_ctx_init.return_value
176 notifier = mock.Mock()
178 stage = self.ConstructStage()
180 # Ensure we find that we are ready if there are no channels to look for.
181 self.assertTrue(stage._CheckForResults(mock_gs_ctx, {}, notifier))
183 # Ensure we didn't contact GS while checking for no channels.
184 self.assertFalse(mock_gs_ctx.Cat.called)
185 self.assertEqual(notifier.mock_calls, [])
187 def testCheckForResultsPartialComplete(self):
188 """Verify _CheckForResults handles partial signing results."""
189 def catChan2Success(url):
190 if url.startswith('chan2'):
192 result.output = self.SIGNER_RESULT
195 raise release_stages.gs.GSNoSuchKey()
197 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
198 mock_gs_ctx = mock_gs_ctx_init.return_value
199 mock_gs_ctx.Cat.side_effect = catChan2Success
200 notifier = mock.Mock()
202 stage = self.ConstructStage()
204 stage._CheckForResults(mock_gs_ctx,
205 self.INSNS_URLS_PER_CHANNEL,
207 self.assertEqual(stage.signing_results, {
213 'keyset': 'link-mp-v4',
214 'status': {'status': 'passed'},
219 self.assertEqual(notifier.mock_calls, [mock.call('chan2')])
221 def testCheckForResultsUnexpectedJson(self):
222 """Verify _CheckForResults handles unexpected Json values."""
223 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
224 mock_gs_ctx = mock_gs_ctx_init.return_value
225 mock_gs_ctx.Cat.return_value.output = "{}"
226 notifier = mock.Mock()
228 stage = self.ConstructStage()
230 stage._CheckForResults(mock_gs_ctx,
231 self.INSNS_URLS_PER_CHANNEL,
233 self.assertEqual(stage.signing_results, {
234 'chan1': {}, 'chan2': {}
236 self.assertEqual(notifier.mock_calls, [])
238 def testCheckForResultsNoResult(self):
239 """Verify _CheckForResults handles missing signer results."""
240 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
241 mock_gs_ctx = mock_gs_ctx_init.return_value
242 mock_gs_ctx.Cat.side_effect = release_stages.gs.GSNoSuchKey
243 notifier = mock.Mock()
245 stage = self.ConstructStage()
247 stage._CheckForResults(mock_gs_ctx,
248 self.INSNS_URLS_PER_CHANNEL,
250 self.assertEqual(stage.signing_results, {
251 'chan1': {}, 'chan2': {}
253 self.assertEqual(notifier.mock_calls, [])
255 def testCheckForResultsFailed(self):
256 """Verify _CheckForResults handles missing signer results."""
257 with patch(release_stages.gs, 'GSContext') as mock_gs_ctx_init:
258 mock_gs_ctx = mock_gs_ctx_init.return_value
259 mock_gs_ctx.Cat.side_effect = release_stages.gs.GSNoSuchKey
260 notifier = mock.Mock()
262 stage = self.ConstructStage()
264 stage._CheckForResults(mock_gs_ctx,
265 self.INSNS_URLS_PER_CHANNEL,
267 self.assertEqual(stage.signing_results, {
268 'chan1': {}, 'chan2': {}
270 self.assertEqual(notifier.mock_calls, [])
272 def generateNotifyCalls(self, channels):
273 def side_effect(_, notifier):
274 for channel in channels:
278 def testPerformStageSuccess(self):
279 """Test that PaygenStage works when signing works."""
281 with patch(release_stages.parallel, 'BackgroundTaskRunner') as background:
282 queue = background().__enter__()
284 # This patch is only required for external builds with no config data.
285 with patch(paygen_build_lib, 'ValidateBoardConfig'):
287 stage = self.ConstructStage()
289 with patch(stage, '_WaitForPushImage') as wait_push:
290 with patch(stage, '_WaitForSigningResults') as wait_signing:
291 wait_push.return_value = self.INSNS_URLS_PER_CHANNEL
292 wait_signing.side_effect = self.generateNotifyCalls(('stable',
296 # Verify that we queue up work
298 queue.put.call_args_list,
299 [mock.call(('stable', 'x86-mario', '0.0.1', False, False, False)),
300 mock.call(('beta', 'x86-mario', '0.0.1', False, False, False))])
302 def testPerformStageSuccessVarientBoard(self):
303 """Test that SignerResultsStage works with varient boards.
305 Varient boards need some name conversion. Make sure that's okay.
307 self._current_board = 'x86-alex_he'
309 with patch(release_stages.parallel, 'BackgroundTaskRunner') as background:
310 queue = background().__enter__()
312 # This patch is only required for external builds with no config data.
313 with patch(paygen_build_lib, 'ValidateBoardConfig'):
314 stage = self.ConstructStage()
316 with patch(stage, '_WaitForPushImage') as wait_push:
317 with patch(stage, '_WaitForSigningResults') as wait_signing:
318 wait_push.return_value = self.INSNS_URLS_PER_CHANNEL
319 wait_signing.side_effect = self.generateNotifyCalls(('stable',
323 # Verify that we queue up work
325 queue.put.call_args_list,
326 [mock.call(('stable', 'x86-alex-he', '0.0.1', False, False, False)),
327 mock.call(('beta', 'x86-alex-he', '0.0.1', False, False, False))])
329 def testPerformStageSigningFailed(self):
330 """Test that PaygenStage works when signing works."""
331 with patch(release_stages.parallel, 'BackgroundTaskRunner') as background:
332 queue = background().__enter__()
334 # This patch is only required for external builds with no config data.
335 with patch(paygen_build_lib, 'ValidateBoardConfig'):
336 stage = self.ConstructStage()
338 with patch(stage, '_WaitForPushImage') as wait_push:
339 with patch(stage, '_WaitForSigningResults') as wait_signing:
340 wait_push.return_value = self.INSNS_URLS_PER_CHANNEL
341 wait_signing.side_effect = release_stages.SignerFailure
343 self.assertRaises(release_stages.SignerFailure,
346 # Ensure no work was queued up.
347 self.assertFalse(queue.put.called)
349 def testPerformStageBackgroundFail(self):
350 """Test that exception from background processes are properly handled."""
351 with patch(paygen_build_lib, 'CreatePayloads') as create_payloads:
352 create_payloads.side_effect = failures_lib.TestLabFailure
354 # This patch is only required for external builds with no config data.
355 with patch(paygen_build_lib, 'ValidateBoardConfig'):
356 stage = release_stages.PaygenStage(
357 self._run, self._current_board,
358 archive_stage=None, channels=['foo', 'bar'])
360 with patch(stage, '_HandleExceptionAsWarning') as warning_handler:
361 warning_handler.return_value = (results_lib.Results.FORGIVEN,
367 # This proves the exception was turned into a warning.
368 self.assertTrue(warning_handler.called)
370 def testPerformStageTrybot(self):
371 """Test the PerformStage alternate behavior for trybot runs."""
372 with patch(release_stages.parallel, 'BackgroundTaskRunner') as background:
373 queue = background().__enter__()
375 # This patch is only required for external builds with no config data.
376 with patch(paygen_build_lib, 'ValidateBoardConfig'):
377 # The stage is constructed differently for trybots, so don't use
379 stage = release_stages.PaygenStage(self._run, self._current_board,
380 archive_stage=None, channels=['foo', 'bar'])
381 with patch(stage, '_WaitForPushImage') as wait_push:
382 with patch(stage, '_WaitForSigningResults') as wait_signing:
385 # Make sure we don't wait on push_image or signing in this case.
386 self.assertEqual(wait_push.mock_calls, [])
387 self.assertEqual(wait_signing.mock_calls, [])
389 # Notice that we didn't put anything in _wait_for_channel_signing, but
390 # still got results right away.
392 queue.put.call_args_list,
393 [mock.call(('foo', 'x86-mario', '0.0.1', False, False, False)),
394 mock.call(('bar', 'x86-mario', '0.0.1', False, False, False))])
396 def testPerformStageUnknownBoard(self):
397 """Test that PaygenStage exits when an unknown board is specified."""
398 self._current_board = 'unknown-board-name'
400 badBoardException = paygen_build_lib.BoardNotConfigured(self._current_board)
402 # This patch is only required for external builds with no config data.
403 with patch(paygen_build_lib, 'ValidateBoardConfig') as validate_boards:
404 validate_boards.side_effect = badBoardException
406 stage = self.ConstructStage()
408 self.assertRaises(release_stages.PaygenNoPaygenConfigForBoard,
411 def testRunPaygenInProcess(self):
412 """Test that _RunPaygenInProcess works in the simple case."""
413 with patch(paygen_build_lib, 'CreatePayloads') as create_payloads:
414 # Call the method under test.
415 stage = self.ConstructStage()
416 stage._RunPaygenInProcess('foo', 'foo-board', 'foo-version',
419 # Ensure arguments are properly converted and passed along.
420 create_payloads.assert_called_with(gspaths.Build(version='foo-version',
422 channel='foo-channel'),
427 skip_delta_payloads=False,
428 skip_test_payloads=False,
431 def testRunPaygenInProcessComplex(self):
432 """Test that _RunPaygenInProcess with arguments that are more unusual."""
433 with patch(paygen_build_lib, 'CreatePayloads') as create_payloads:
434 # Call the method under test.
435 # Use release tools channel naming, and a board name including a variant.
436 stage = self.ConstructStage()
437 stage._RunPaygenInProcess('foo-channel', 'foo-board-variant',
438 'foo-version', True, True, True)
440 # Ensure arguments are properly converted and passed along.
441 create_payloads.assert_called_with(
442 gspaths.Build(version='foo-version',
443 board='foo-board-variant',
444 channel='foo-channel'),
449 skip_delta_payloads=True,
450 skip_test_payloads=True,
453 if __name__ == '__main__':