| 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 |