| Index: appengine/swarming/swarming_bot/bot_code/remote_client_grpc_test.py
|
| diff --git a/appengine/swarming/swarming_bot/bot_code/remote_client_grpc_test.py b/appengine/swarming/swarming_bot/bot_code/remote_client_grpc_test.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..30085ee0c2b7c725594fd0142fdd1b65522e6eeb
|
| --- /dev/null
|
| +++ b/appengine/swarming/swarming_bot/bot_code/remote_client_grpc_test.py
|
| @@ -0,0 +1,254 @@
|
| +#!/usr/bin/env python
|
| +# Copyright 2016 The LUCI Authors. All rights reserved.
|
| +# Use of this source code is governed under the Apache License, Version 2.0
|
| +# that can be found in the LICENSE file.
|
| +
|
| +import logging
|
| +import sys
|
| +import threading
|
| +import time
|
| +import unittest
|
| +
|
| +import test_env_bot_code
|
| +test_env_bot_code.setup_test_env()
|
| +
|
| +from depot_tools import auto_stub
|
| +
|
| +try:
|
| + import remote_client_grpc
|
| +except ImportError as e:
|
| + print('Could not import gRPC remote client, likely due to missing grpc '
|
| + 'library. Skipping tests.')
|
| + sys.exit(0)
|
| +
|
| +
|
| +class FakeBotServiceStub(object):
|
| + def __init__(self, testobj):
|
| + self._testobj = testobj
|
| +
|
| + def TaskUpdate(self, request, **_kwargs):
|
| + return self._testobj._handle_call('TaskUpdate', request)
|
| +
|
| + def Handshake(self, request, **_kwargs):
|
| + return self._testobj._handle_call('Handshake', request)
|
| +
|
| + def Poll(self, request, **_kwargs):
|
| + return self._testobj._handle_call('Poll', request)
|
| +
|
| +
|
| +class TestRemoteClientGrpc(auto_stub.TestCase):
|
| + def setUp(self):
|
| + super(TestRemoteClientGrpc, self).setUp()
|
| + self._client = remote_client_grpc.RemoteClientGrpc('1.2.3.4:90')
|
| + self._client._stub = FakeBotServiceStub(self)
|
| + self._expected = []
|
| +
|
| + def _handle_call(self, method, request):
|
| + """This is called by FakeBotServiceStub to implement fake calls"""
|
| + # Pop off the first item on the list
|
| + self.assertTrue(len(self._expected) > 0)
|
| + expected, self._expected = self._expected[0], self._expected[1:]
|
| + # Each element of the "expected" array should be a 3-tuple:
|
| + # * The name of the method (eg 'TaskUpdate')
|
| + # * The proto request
|
| + # * The proto response
|
| + self.assertEqual(method, expected[0])
|
| + self.assertEqual(request, expected[1])
|
| + return expected[2]
|
| +
|
| + def get_bot_attributes_dict(self):
|
| + """Gets the attributes of a basic bot"""
|
| + return {
|
| + 'version': '123',
|
| + 'dimensions': {
|
| + 'mammal': ['ferrett', 'wombat'],
|
| + 'pool': ['dead'],
|
| + },
|
| + 'state': {
|
| + 'audio': ['Rachmaninov', 'Stravinsky'],
|
| + 'cost_usd_hour': 3.141,
|
| + 'cpu': 'Intel 8080',
|
| + 'disks': {
|
| + 'mount': 'path/to/mount',
|
| + 'volume': 'turned-to-eleven',
|
| + },
|
| + 'sleep_streak': 8,
|
| + },
|
| + }
|
| +
|
| + def get_bot_attributes_proto(self):
|
| + """Gets the attributes proto of a basic bot; must match prior function"""
|
| + attributes = remote_client_grpc.swarming_bot_pb2.Attributes()
|
| + attributes.version = '123'
|
| + # Note that dimensions are sorted
|
| + d1 = attributes.dimensions.add()
|
| + d1.name = 'mammal'
|
| + d1.values.extend(['ferrett', 'wombat'])
|
| + d2 = attributes.dimensions.add()
|
| + d2.name = 'pool'
|
| + d2.values.append('dead')
|
| + attributes.state.audio.extend(['Rachmaninov', 'Stravinsky'])
|
| + attributes.state.cost_usd_hour = 3.141
|
| + attributes.state.cpu = 'Intel 8080'
|
| + attributes.state.sleep_streak = 8
|
| + disk1 = attributes.state.disks.fields['mount']
|
| + disk1.string_value = 'path/to/mount'
|
| + disk2 = attributes.state.disks.fields['volume']
|
| + disk2.string_value = 'turned-to-eleven'
|
| + return attributes
|
| +
|
| + def test_handshake(self):
|
| + """Tests the handshake proto"""
|
| + msg_req = remote_client_grpc.swarming_bot_pb2.HandshakeRequest()
|
| + msg_req.attributes.CopyFrom(self.get_bot_attributes_proto())
|
| +
|
| + # Create proto response
|
| + msg_rsp = remote_client_grpc.swarming_bot_pb2.HandshakeResponse()
|
| + msg_rsp.server_version = '101'
|
| + msg_rsp.bot_version = '102'
|
| + d1 = msg_rsp.bot_group_cfg.dimensions.add()
|
| + d1.name = 'mammal'
|
| + d1.values.extend(['kangaroo', 'emu'])
|
| +
|
| + # Execute call and verify response
|
| + expected_call = ('Handshake', msg_req, msg_rsp)
|
| + self._expected.append(expected_call)
|
| + response = self._client.do_handshake(self.get_bot_attributes_dict())
|
| + self.assertEqual(response, {
|
| + 'server_version': u'101',
|
| + 'bot_version': u'102',
|
| + 'bot_group_cfg_version': u'',
|
| + 'bot_group_cfg': {
|
| + 'dimensions': {
|
| + u'mammal': [u'kangaroo', u'emu'],
|
| + },
|
| + },
|
| + })
|
| +
|
| + def test_poll_manifest(self):
|
| + """Verifies that we can generate a reasonable manifest from a proto"""
|
| + msg_req = remote_client_grpc.swarming_bot_pb2.PollRequest()
|
| + msg_req.attributes.CopyFrom(self.get_bot_attributes_proto())
|
| +
|
| + # Create dict response
|
| + dict_rsp = {
|
| + 'bot_id': u'baby_bot',
|
| + 'command': None,
|
| + 'dimensions': {
|
| + u'mammal': u'emu',
|
| + u'pool': u'dead',
|
| + },
|
| + 'env': {
|
| + u'co2': u'400ppm',
|
| + u'n2': u'80%',
|
| + u'o2': u'20%',
|
| + },
|
| + 'extra_args': None,
|
| + 'grace_period': 1,
|
| + 'hard_timeout': 2,
|
| + 'io_timeout': 3,
|
| + 'isolated': {
|
| + 'namespace': u'default-gzip',
|
| + 'input': u'123abc',
|
| + 'server': '1.2.3.4:90', # from when client was created
|
| + },
|
| + 'outputs': [u'foo.o', u'foo.log'],
|
| + 'task_id': u'ifyouchoosetoacceptit',
|
| + }
|
| + # Create proto response
|
| + msg_rsp = remote_client_grpc.swarming_bot_pb2.PollResponse()
|
| + msg_rsp.cmd = remote_client_grpc.swarming_bot_pb2.PollResponse.RUN
|
| + msg_rsp.manifest.bot_id = 'baby_bot'
|
| + msg_rsp.manifest.dimensions['mammal'] = 'emu'
|
| + msg_rsp.manifest.dimensions['pool'] = 'dead'
|
| + msg_rsp.manifest.env['co2'] = '400ppm'
|
| + msg_rsp.manifest.env['n2'] = '80%'
|
| + msg_rsp.manifest.env['o2'] = '20%'
|
| + msg_rsp.manifest.grace_period = 1
|
| + msg_rsp.manifest.hard_timeout = 2
|
| + msg_rsp.manifest.io_timeout = 3
|
| + msg_rsp.manifest.isolated.namespace = 'default-gzip'
|
| + msg_rsp.manifest.isolated.input = '123abc'
|
| + msg_rsp.manifest.outputs.extend(['foo.o', 'foo.log'])
|
| + msg_rsp.manifest.task_id = 'ifyouchoosetoacceptit'
|
| +
|
| + # Execute call and verify response
|
| + expected_call = ('Poll', msg_req, msg_rsp)
|
| + self._expected.append(expected_call)
|
| + cmd, value = self._client.poll(self.get_bot_attributes_dict())
|
| + self.assertEqual(cmd, 'run')
|
| + self.assertEqual(value, dict_rsp)
|
| +
|
| + def test_update_no_output_or_code(self):
|
| + """Includes only a basic param set"""
|
| + # Create params dict
|
| + params = {
|
| + 'bot_overhead': 3.141,
|
| + 'cost_usd': 0.001,
|
| + }
|
| +
|
| + # Create expected request proto
|
| + msg_req = remote_client_grpc.swarming_bot_pb2.TaskUpdateRequest()
|
| + msg_req.id = 'baby_bot'
|
| + msg_req.task_id = 'abc123'
|
| + msg_req.bot_overhead = 3.141
|
| + msg_req.cost_usd = 0.001
|
| +
|
| + # Create proto response
|
| + msg_rsp = remote_client_grpc.swarming_bot_pb2.TaskUpdateResponse()
|
| +
|
| + # Execute call and verify response
|
| + expected_call = ('TaskUpdate', msg_req, msg_rsp)
|
| + self._expected.append(expected_call)
|
| + response = self._client.post_task_update('abc123', 'baby_bot', params)
|
| + self.assertTrue(response)
|
| +
|
| + def test_update_final(self):
|
| + """Includes output chunk, exit code and full param set"""
|
| + # Create params dict
|
| + params = {
|
| + 'outputs_ref': {
|
| + 'isolated': 'def987',
|
| + 'isolatedserver': 'foo',
|
| + 'namespace': 'default-gzip',
|
| + },
|
| + 'isolated_stats': {
|
| + 'upload': {
|
| + 'duration': 0.5,
|
| + 'items_cold': 'YWJj', # b64(u'abc')
|
| + },
|
| + 'download': {
|
| + 'initial_size': 1234567890,
|
| + },
|
| + },
|
| + }
|
| +
|
| + # Create expected request proto
|
| + msg_req = remote_client_grpc.swarming_bot_pb2.TaskUpdateRequest()
|
| + msg_req.id = 'baby_bot'
|
| + msg_req.task_id = 'abc123'
|
| + msg_req.exit_status.code = -5
|
| + msg_req.output_chunk.data = 'abc'
|
| + msg_req.output_chunk.offset = 7
|
| + msg_req.outputs_ref.isolated = 'def987'
|
| + msg_req.outputs_ref.isolatedserver = 'foo'
|
| + msg_req.outputs_ref.namespace = 'default-gzip'
|
| + msg_req.isolated_stats.upload.duration = 0.5
|
| + msg_req.isolated_stats.upload.items_cold = 'abc'
|
| + msg_req.isolated_stats.download.initial_size = 1234567890
|
| +
|
| + # Create proto response
|
| + msg_rsp = remote_client_grpc.swarming_bot_pb2.TaskUpdateResponse()
|
| +
|
| + # Execute call and verify response
|
| + expected_call = ('TaskUpdate', msg_req, msg_rsp)
|
| + self._expected.append(expected_call)
|
| + response = self._client.post_task_update('abc123', 'baby_bot', params,
|
| + ['abc', 7], -5)
|
| + self.assertTrue(response)
|
| +
|
| +if __name__ == '__main__':
|
| + logging.basicConfig(
|
| + level=logging.DEBUG if '-v' in sys.argv else logging.CRITICAL)
|
| + unittest.TestCase.maxDiff = None
|
| + unittest.main()
|
|
|