OLD | NEW |
1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import base64 | 5 import base64 |
6 import contextlib | 6 import contextlib |
7 import datetime | 7 import datetime |
8 import json | 8 import json |
| 9 import logging |
9 | 10 |
10 from components import auth | 11 from components import auth |
11 from components import config as config_component | 12 from components import config as config_component |
12 from components import net | 13 from components import net |
13 from components import utils | 14 from components import utils |
14 from google.appengine.ext import ndb | 15 from google.appengine.ext import ndb |
15 from testing_utils import testing | 16 from testing_utils import testing |
16 from webob import exc | 17 from webob import exc |
17 import mock | 18 import mock |
18 import webapp2 | 19 import webapp2 |
19 | 20 |
20 from swarming import swarming | 21 from swarming import swarming |
21 from proto import project_config_pb2 | 22 from proto import project_config_pb2 |
22 import config | 23 import config |
23 import errors | 24 import errors |
24 import model | 25 import model |
25 | 26 |
26 | 27 |
27 def futuristic(result): | 28 def futuristic(result): |
28 f = ndb.Future() | 29 f = ndb.Future() |
29 f.set_result(result) | 30 f.set_result(result) |
30 return f | 31 return f |
31 | 32 |
32 | 33 |
33 class SwarmingTest(testing.AppengineTestCase): | 34 class SwarmingTest(testing.AppengineTestCase): |
34 def setUp(self): | 35 def setUp(self): |
35 super(SwarmingTest, self).setUp() | 36 super(SwarmingTest, self).setUp() |
36 self.mock(utils, 'utcnow', lambda: datetime.datetime(2015, 11, 30)) | 37 self.mock(utils, 'utcnow', lambda: datetime.datetime(2015, 11, 30)) |
| 38 |
| 39 self.json_response = None |
| 40 def json_request_async(*_, **__): |
| 41 if self.json_response is not None: |
| 42 return futuristic(self.json_response) |
| 43 self.fail('unexpected outbound request') # pragma: no cover |
| 44 |
| 45 self.mock( |
| 46 net, 'json_request_async', mock.Mock(side_effect=json_request_async)) |
| 47 |
37 self.bucket_cfg = project_config_pb2.Bucket( | 48 self.bucket_cfg = project_config_pb2.Bucket( |
38 name='bucket', | 49 name='bucket', |
39 swarming=project_config_pb2.Swarming( | 50 swarming=project_config_pb2.Swarming( |
40 hostname='chromium-swarm.appspot.com', | 51 hostname='chromium-swarm.appspot.com', |
41 url_format='https://example.com/{swarming_hostname}/{task_id}', | 52 url_format='https://example.com/{swarming_hostname}/{task_id}', |
42 common_swarming_tags=['commontag:yes'], | 53 common_swarming_tags=['commontag:yes'], |
43 common_dimensions=['cores:8', 'pool:default', 'cpu:x86-64'], | 54 common_dimensions=['cores:8', 'pool:default', 'cpu:x86-64'], |
44 common_recipe=project_config_pb2.Swarming.Recipe( | 55 common_recipe=project_config_pb2.Swarming.Recipe( |
45 repository='https://example.com/repo', | 56 repository='https://example.com/repo', |
46 name='recipe', | 57 name='recipe', |
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
140 {'changes': [{'author': {'email': 0}}]}, | 151 {'changes': [{'author': {'email': 0}}]}, |
141 {'changes': [{'author': {'email': ''}}]}, | 152 {'changes': [{'author': {'email': ''}}]}, |
142 {'changes': [{'author': {'email': 'a@example.com'}, 'repo_url': 0}]}, | 153 {'changes': [{'author': {'email': 'a@example.com'}, 'repo_url': 0}]}, |
143 {'bad key': 1}, | 154 {'bad key': 1}, |
144 {'swarming': []}, | 155 {'swarming': []}, |
145 {'swarming': {'junk': 1}}, | 156 {'swarming': {'junk': 1}}, |
146 {'swarming': {'recipe': []}}, | 157 {'swarming': {'recipe': []}}, |
147 {'swarming': {'recipe': {'junk': 1}}}, | 158 {'swarming': {'recipe': {'junk': 1}}}, |
148 {'swarming': {'recipe': {'revision': 1}}}, | 159 {'swarming': {'recipe': {'revision': 1}}}, |
149 {'swarming': {'canary_template': 'yes'}}, | 160 {'swarming': {'canary_template': 'yes'}}, |
| 161 {'swarming': {'override_cfg': 0}}, |
| 162 {'swarming': {'override_cfg': {'swarming': 0}}}, |
| 163 { |
| 164 'swarming': { |
| 165 'override_cfg': { |
| 166 'swarming': {'hostname': 0}, |
| 167 }, |
| 168 }, |
| 169 }, |
| 170 { |
| 171 'swarming': { |
| 172 'override_cfg': { |
| 173 'swarming': {'builders': [{}]}, |
| 174 }, |
| 175 }, |
| 176 }, |
| 177 { |
| 178 'swarming': { |
| 179 'override_cfg': { |
| 180 'swarming': {'common_dimensions': ['wrong_dimension']}, |
| 181 }, |
| 182 }, |
| 183 }, |
| 184 {'swarming': {'override_cfg': {'x': 0}}}, |
150 ] | 185 ] |
151 for p in bad: | 186 for p in bad: |
| 187 logging.info('testing %s', p) |
152 p['builder_name'] = 'foo' | 188 p['builder_name'] = 'foo' |
153 with self.assertRaises(errors.InvalidInputError): | 189 with self.assertRaises(errors.InvalidInputError): |
154 swarming.validate_build_parameters(p['builder_name'], p) | 190 swarming.validate_build_parameters(p['builder_name'], p) |
155 | 191 |
156 def test_execution_timeout(self): | 192 def test_execution_timeout(self): |
157 self.bucket_cfg.swarming.common_execution_timeout_secs = 120 | 193 self.bucket_cfg.swarming.common_execution_timeout_secs = 120 |
158 builder_cfg = project_config_pb2.Swarming.Builder(name='fast-builder') | 194 builder_cfg = project_config_pb2.Swarming.Builder(name='fast-builder') |
159 | 195 |
160 build = model.Build( | 196 build = model.Build( |
161 bucket='bucket', | 197 bucket='bucket', |
(...skipping 27 matching lines...) Expand all Loading... |
189 'properties': { | 225 'properties': { |
190 'a': 'b', | 226 'a': 'b', |
191 }, | 227 }, |
192 'changes': [{ | 228 'changes': [{ |
193 'author': {'email': 'bob@example.com'}, | 229 'author': {'email': 'bob@example.com'}, |
194 'repo_url': 'https://chromium.googlesource.com/chromium/src', | 230 'repo_url': 'https://chromium.googlesource.com/chromium/src', |
195 }] | 231 }] |
196 }, | 232 }, |
197 ) | 233 ) |
198 | 234 |
199 self.mock(net, 'json_request_async', mock.Mock(return_value=futuristic({ | 235 self.json_response = { |
200 'task_id': 'deadbeef', | 236 'task_id': 'deadbeef', |
201 'request': { | 237 'request': { |
202 'properties': { | 238 'properties': { |
203 'dimensions': [ | 239 'dimensions': [ |
204 {'key': 'cores', 'value': '8'}, | 240 {'key': 'cores', 'value': '8'}, |
205 {'key': 'os', 'value': 'Linux'}, | 241 {'key': 'os', 'value': 'Linux'}, |
206 {'key': 'pool', 'value': 'Chrome'}, | 242 {'key': 'pool', 'value': 'Chrome'}, |
207 ], | 243 ], |
208 }, | 244 }, |
209 'tags': [ | 245 'tags': [ |
210 'builder:builder', | 246 'builder:builder', |
211 'buildertag:yes', | 247 'buildertag:yes', |
212 'commontag:yes', | 248 'commontag:yes', |
213 'master:master.bucket', | 249 'master:master.bucket', |
214 'priority:108', | 250 'priority:108', |
215 'recipe_name:recipe', | 251 'recipe_name:recipe', |
216 'recipe_repository:https://example.com/repo', | 252 'recipe_repository:https://example.com/repo', |
217 'recipe_revision:badcoffee', | 253 'recipe_revision:badcoffee', |
218 ] | 254 ] |
219 } | 255 } |
220 }))) | 256 } |
221 | 257 |
222 swarming.create_task_async(build).get_result() | 258 swarming.create_task_async(build).get_result() |
223 | 259 |
224 # Test swarming request. | 260 # Test swarming request. |
225 self.assertEqual( | 261 self.assertEqual( |
226 net.json_request_async.call_args[0][0], | 262 net.json_request_async.call_args[0][0], |
227 'https://chromium-swarm.appspot.com/_ah/api/swarming/v1/tasks/new') | 263 'https://chromium-swarm.appspot.com/_ah/api/swarming/v1/tasks/new') |
228 actual_task_def = net.json_request_async.call_args[1]['payload'] | 264 actual_task_def = net.json_request_async.call_args[1]['payload'] |
229 del actual_task_def['pubsub_auth_token'] | 265 del actual_task_def['pubsub_auth_token'] |
230 expected_task_def = { | 266 expected_task_def = { |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
301 def test_create_task_async_canary_template(self): | 337 def test_create_task_async_canary_template(self): |
302 build = model.Build( | 338 build = model.Build( |
303 bucket='bucket', | 339 bucket='bucket', |
304 parameters={ | 340 parameters={ |
305 'builder_name': 'builder', | 341 'builder_name': 'builder', |
306 'swarming': { | 342 'swarming': { |
307 'canary_template': True, | 343 'canary_template': True, |
308 } }, | 344 } }, |
309 ) | 345 ) |
310 | 346 |
311 self.mock(net, 'json_request_async', mock.Mock(return_value=futuristic({ | 347 self.json_response = { |
312 'task_id': 'deadbeef', | 348 'task_id': 'deadbeef', |
313 'request': { | 349 'request': { |
314 'properties': { | 350 'properties': { |
315 'dimensions': [ | 351 'dimensions': [ |
316 {'key': 'cores', 'value': '8'}, | 352 {'key': 'cores', 'value': '8'}, |
317 {'key': 'os', 'value': 'Linux'}, | 353 {'key': 'os', 'value': 'Linux'}, |
318 {'key': 'pool', 'value': 'Chrome'}, | 354 {'key': 'pool', 'value': 'Chrome'}, |
319 ], | 355 ], |
320 }, | 356 }, |
321 'tags': [ | 357 'tags': [ |
322 'builder:builder', | 358 'builder:builder', |
323 'buildertag:yes', | 359 'buildertag:yes', |
324 'commontag:yes', | 360 'commontag:yes', |
325 'master:master.bucket', | 361 'master:master.bucket', |
326 'priority:108', | 362 'priority:108', |
327 'recipe_name:recipe', | 363 'recipe_name:recipe', |
328 'recipe_repository:https://example.com/repo', | 364 'recipe_repository:https://example.com/repo', |
329 ] | 365 ] |
330 } | 366 } |
331 }))) | 367 } |
332 | 368 |
333 swarming.create_task_async(build).get_result() | 369 swarming.create_task_async(build).get_result() |
334 | 370 |
335 # Test swarming request. | 371 # Test swarming request. |
336 self.assertEqual( | 372 self.assertEqual( |
337 net.json_request_async.call_args[0][0], | 373 net.json_request_async.call_args[0][0], |
338 'https://chromium-swarm.appspot.com/_ah/api/swarming/v1/tasks/new') | 374 'https://chromium-swarm.appspot.com/_ah/api/swarming/v1/tasks/new') |
339 actual_task_def = net.json_request_async.call_args[1]['payload'] | 375 actual_task_def = net.json_request_async.call_args[1]['payload'] |
340 del actual_task_def['pubsub_auth_token'] | 376 del actual_task_def['pubsub_auth_token'] |
341 expected_task_def = { | 377 expected_task_def = { |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
418 | 454 |
419 self.task_template_canary = None | 455 self.task_template_canary = None |
420 with self.assertRaises(errors.InvalidInputError): | 456 with self.assertRaises(errors.InvalidInputError): |
421 swarming.create_task_async(build).get_result() | 457 swarming.create_task_async(build).get_result() |
422 | 458 |
423 def test_create_task_async_no_canary_template_implicit(self): | 459 def test_create_task_async_no_canary_template_implicit(self): |
424 self.mock(swarming, 'should_use_canary_template', mock.Mock()) | 460 self.mock(swarming, 'should_use_canary_template', mock.Mock()) |
425 swarming.should_use_canary_template.return_value = True | 461 swarming.should_use_canary_template.return_value = True |
426 self.task_template_canary = None | 462 self.task_template_canary = None |
427 | 463 |
428 self.mock(net, 'json_request_async', mock.Mock(return_value=futuristic({ | 464 self.json_response = { |
429 'task_id': 'deadbeef', | 465 'task_id': 'deadbeef', |
430 'request': { | 466 'request': { |
431 'properties': { | 467 'properties': { |
432 'dimensions': [ | 468 'dimensions': [ |
433 {'key': 'cores', 'value': '8'}, | 469 {'key': 'cores', 'value': '8'}, |
434 {'key': 'os', 'value': 'Linux'}, | 470 {'key': 'os', 'value': 'Linux'}, |
435 {'key': 'pool', 'value': 'Chrome'}, | 471 {'key': 'pool', 'value': 'Chrome'}, |
436 ], | 472 ], |
437 }, | 473 }, |
438 'tags': [ | 474 'tags': [ |
439 'builder:builder', | 475 'builder:builder', |
440 'buildertag:yes', | 476 'buildertag:yes', |
441 'commontag:yes', | 477 'commontag:yes', |
442 'master:master.bucket', | 478 'master:master.bucket', |
443 'priority:108', | 479 'priority:108', |
444 'recipe_name:recipe', | 480 'recipe_name:recipe', |
445 'recipe_repository:https://example.com/repo', | 481 'recipe_repository:https://example.com/repo', |
446 'recipe_revision:badcoffee', | 482 'recipe_revision:badcoffee', |
447 ] | 483 ] |
448 } | 484 } |
449 }))) | 485 } |
450 | 486 |
451 build = model.Build( | 487 build = model.Build( |
452 bucket='bucket', | 488 bucket='bucket', |
453 parameters={'builder_name': 'builder'}, | 489 parameters={'builder_name': 'builder'}, |
454 ) | 490 ) |
455 swarming.create_task_async(build).get_result() | 491 swarming.create_task_async(build).get_result() |
456 | 492 |
457 actual_task_def = net.json_request_async.call_args[1]['payload'] | 493 actual_task_def = net.json_request_async.call_args[1]['payload'] |
458 self.assertIn('buildbucket_template_canary:false', actual_task_def['tags']) | 494 self.assertIn('buildbucket_template_canary:false', actual_task_def['tags']) |
459 | 495 |
| 496 def test_create_task_async_override_cfg(self): |
| 497 build = model.Build( |
| 498 bucket='bucket', |
| 499 parameters={ |
| 500 'builder_name': 'builder', |
| 501 'swarming': { |
| 502 'override_builder_cfg': { |
| 503 # Override cores dimension. |
| 504 'dimensions': ['cores:16'], |
| 505 }, |
| 506 } |
| 507 }, |
| 508 ) |
| 509 |
| 510 self.json_response = { |
| 511 'task_id': 'deadbeef', |
| 512 'request': { |
| 513 'properties': { |
| 514 'dimensions': [ |
| 515 {'key': 'cores', 'value': '16'}, |
| 516 {'key': 'os', 'value': 'Linux'}, |
| 517 {'key': 'pool', 'value': 'Chrome'}, |
| 518 ], |
| 519 }, |
| 520 'tags': [ |
| 521 'builder:builder', |
| 522 'buildertag:yes', |
| 523 'commontag:yes', |
| 524 'master:master.bucket', |
| 525 'priority:108', |
| 526 'recipe_name:recipe', |
| 527 'recipe_repository:https://example.com/repo', |
| 528 'recipe_revision:badcoffee', |
| 529 ] |
| 530 } |
| 531 } |
| 532 |
| 533 swarming.create_task_async(build).get_result() |
| 534 |
| 535 actual_task_def = net.json_request_async.call_args[1]['payload'] |
| 536 self.assertIn( |
| 537 {'key': 'cores', 'value': '16'}, |
| 538 actual_task_def['properties']['dimensions']) |
| 539 |
| 540 def test_create_task_async_override_cfg_malformed(self): |
| 541 build = model.Build( |
| 542 bucket='bucket', |
| 543 parameters={ |
| 544 'builder_name': 'builder', |
| 545 'swarming': { |
| 546 'override_builder_cfg': [], |
| 547 } |
| 548 }, |
| 549 ) |
| 550 with self.assertRaises(errors.InvalidInputError): |
| 551 swarming.create_task_async(build).get_result() |
| 552 |
| 553 build = model.Build( |
| 554 bucket='bucket', |
| 555 parameters={ |
| 556 'builder_name': 'builder', |
| 557 'swarming': { |
| 558 'override_builder_cfg': { |
| 559 'name': 'x', |
| 560 }, |
| 561 } |
| 562 }, |
| 563 ) |
| 564 with self.assertRaises(errors.InvalidInputError): |
| 565 swarming.create_task_async(build).get_result() |
| 566 |
| 567 build = model.Build( |
| 568 bucket='bucket', |
| 569 parameters={ |
| 570 'builder_name': 'builder', |
| 571 'swarming': { |
| 572 'override_builder_cfg': { |
| 573 'blabla': 'x', |
| 574 }, |
| 575 } |
| 576 }, |
| 577 ) |
| 578 with self.assertRaises(errors.InvalidInputError): |
| 579 swarming.create_task_async(build).get_result() |
| 580 |
| 581 # Remove a required dimension. |
| 582 build = model.Build( |
| 583 bucket='bucket', |
| 584 parameters={ |
| 585 'builder_name': 'builder', |
| 586 'swarming': { |
| 587 'override_builder_cfg': { |
| 588 'dimensions': ['pool:'], |
| 589 }, |
| 590 } |
| 591 }, |
| 592 ) |
| 593 with self.assertRaises(errors.InvalidInputError): |
| 594 swarming.create_task_async(build).get_result() |
| 595 |
| 596 |
460 def test_create_task_async_on_leased_build(self): | 597 def test_create_task_async_on_leased_build(self): |
461 build = model.Build( | 598 build = model.Build( |
462 bucket='bucket', | 599 bucket='bucket', |
463 parameters={'builder_name': 'builder'}, | 600 parameters={'builder_name': 'builder'}, |
464 lease_key=12345, | 601 lease_key=12345, |
465 ) | 602 ) |
466 with self.assertRaises(errors.InvalidInputError): | 603 with self.assertRaises(errors.InvalidInputError): |
467 swarming.create_task_async(build).get_result() | 604 swarming.create_task_async(build).get_result() |
468 | 605 |
469 def test_cancel_task(self): | 606 def test_cancel_task(self): |
470 self.mock(net, 'json_request_async', mock.Mock(return_value=futuristic({}))) | 607 self.json_response = {} |
471 build = model.Build( | 608 build = model.Build( |
472 bucket='whatever', | 609 bucket='whatever', |
473 swarming_hostname='chromium-swarm.appspot.com', | 610 swarming_hostname='chromium-swarm.appspot.com', |
474 swarming_task_id='deadbeef', | 611 swarming_task_id='deadbeef', |
475 ) | 612 ) |
476 swarming.cancel_task_async(build).get_result() | 613 swarming.cancel_task_async(build).get_result() |
477 net.json_request_async.assert_called_with( | 614 net.json_request_async.assert_called_with( |
478 ('https://chromium-swarm.appspot.com/' | 615 ('https://chromium-swarm.appspot.com/' |
479 '_ah/api/swarming/v1/task/deadbeef/cancel'), | 616 '_ah/api/swarming/v1/task/deadbeef/cancel'), |
480 method='POST', | 617 method='POST', |
(...skipping 303 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
784 self.assertEqual(build.status, model.BuildStatus.COMPLETED) | 921 self.assertEqual(build.status, model.BuildStatus.COMPLETED) |
785 self.assertEqual(build.result, model.BuildResult.FAILURE) | 922 self.assertEqual(build.result, model.BuildResult.FAILURE) |
786 self.assertEqual(build.failure_reason, model.FailureReason.INFRA_FAILURE) | 923 self.assertEqual(build.failure_reason, model.FailureReason.INFRA_FAILURE) |
787 self.assertIsNotNone(build.result_details) | 924 self.assertIsNotNone(build.result_details) |
788 self.assertIsNone(build.lease_key) | 925 self.assertIsNone(build.lease_key) |
789 self.assertIsNotNone(build.complete_time) | 926 self.assertIsNotNone(build.complete_time) |
790 | 927 |
791 | 928 |
792 def b64json(data): | 929 def b64json(data): |
793 return base64.b64encode(json.dumps(data)) | 930 return base64.b64encode(json.dumps(data)) |
OLD | NEW |