finnball pushed to branch finn/unittests-bot-interface at BuildGrid / buildgrid
Commits:
-
9d1bccd7
by Carter Sande at 2018-07-26T16:48:20Z
-
38c6b455
by finn at 2018-07-27T09:19:31Z
-
6839e096
by finn at 2018-07-27T10:07:52Z
6 changed files:
- .gitlab-ci.yml
- app/commands/cmd_bot.py
- buildgrid/bot/bot.py
- buildgrid/server/cas/storage/disk.py
- tests/cas/test_services.py
- + tests/integration/bot_interface.py
Changes:
| ... | ... | @@ -11,12 +11,11 @@ before_script: |
| 11 | 11 |
- export PATH=~/.local/bin:${PATH}
|
| 12 | 12 |
- pip3 install --user -e .
|
| 13 | 13 |
|
| 14 |
-tests-fedora:
|
|
| 14 |
+.tests-template: &linux-tests
|
|
| 15 | 15 |
stage: test
|
| 16 | 16 |
variables:
|
| 17 | 17 |
PYTEST_ADDOPTS: "--color=yes"
|
| 18 | 18 |
script:
|
| 19 |
- - yum -y install clang libffi-devel openssl-devel python3-devel
|
|
| 20 | 19 |
- python3 setup.py test
|
| 21 | 20 |
- mkdir -p coverage/
|
| 22 | 21 |
- cp .coverage.* coverage/coverage."${CI_JOB_NAME}"
|
| ... | ... | @@ -24,15 +23,34 @@ tests-fedora: |
| 24 | 23 |
paths:
|
| 25 | 24 |
- coverage/
|
| 26 | 25 |
|
| 27 |
- |
|
| 28 |
-tests-dummy-job-fedora:
|
|
| 26 |
+.run-dummy-job-template: &dummy-job
|
|
| 29 | 27 |
stage: test
|
| 30 | 28 |
script:
|
| 31 | 29 |
- ${BGD} server start &
|
| 32 | 30 |
- sleep 1 # Allow server to boot
|
| 33 |
- - ${BGD} bot --host=0.0.0.0 dummy &
|
|
| 31 |
+ - ${BGD} bot --host=0.0.0.0 --continuous dummy &
|
|
| 34 | 32 |
- ${BGD} execute --host=0.0.0.0 request --wait-for-completion
|
| 35 | 33 |
|
| 34 |
+tests-debian:
|
|
| 35 |
+ image: buildstream/buildstream-debian
|
|
| 36 |
+ <<: *linux-tests
|
|
| 37 |
+ |
|
| 38 |
+# Need to yum install until we have our own image
|
|
| 39 |
+tests-fedora:
|
|
| 40 |
+ <<: *linux-tests
|
|
| 41 |
+ script:
|
|
| 42 |
+ - yum -y install clang libffi-devel openssl-devel python3-devel
|
|
| 43 |
+ - python3 setup.py test
|
|
| 44 |
+ - mkdir -p coverage/
|
|
| 45 |
+ - cp .coverage.* coverage/coverage."${CI_JOB_NAME}"
|
|
| 46 |
+ |
|
| 47 |
+run-dummy-job-debian:
|
|
| 48 |
+ image: buildstream/buildstream-debian
|
|
| 49 |
+ <<: *dummy-job
|
|
| 50 |
+ |
|
| 51 |
+run-dummy-job-fedora:
|
|
| 52 |
+ <<: *dummy-job
|
|
| 53 |
+ |
|
| 36 | 54 |
coverage:
|
| 37 | 55 |
stage: post
|
| 38 | 56 |
coverage: '/TOTAL +\d+ +\d+ +(\d+\.\d+)%/'
|
| ... | ... | @@ -43,15 +43,17 @@ from google.devtools.remoteexecution.v1test import remote_execution_pb2, remote_ |
| 43 | 43 |
from google.protobuf import any_pb2
|
| 44 | 44 |
|
| 45 | 45 |
@click.group(short_help = 'Create a bot client')
|
| 46 |
+@click.option('--continuous', is_flag=True)
|
|
| 46 | 47 |
@click.option('--parent', default='bgd_test')
|
| 47 | 48 |
@click.option('--number-of-leases', default=1)
|
| 48 | 49 |
@click.option('--port', default='50051')
|
| 49 | 50 |
@click.option('--host', default='localhost')
|
| 50 | 51 |
@pass_context
|
| 51 |
-def cli(context, host, port, number_of_leases, parent):
|
|
| 52 |
+def cli(context, host, port, number_of_leases, parent, continuous):
|
|
| 52 | 53 |
context.logger = logging.getLogger(__name__)
|
| 53 | 54 |
context.logger.info("Starting on port {}".format(port))
|
| 54 | 55 |
|
| 56 |
+ context.continuous = continuous
|
|
| 55 | 57 |
context.channel = grpc.insecure_channel('{}:{}'.format(host, port))
|
| 56 | 58 |
context.number_of_leases = number_of_leases
|
| 57 | 59 |
context.parent = parent
|
| ... | ... | @@ -71,7 +73,8 @@ def dummy(context): |
| 71 | 73 |
context=context,
|
| 72 | 74 |
channel=context.channel,
|
| 73 | 75 |
parent=context.parent,
|
| 74 |
- number_of_leases=context.number_of_leases)
|
|
| 76 |
+ number_of_leases=context.number_of_leases,
|
|
| 77 |
+ continuous=context.continuous)
|
|
| 75 | 78 |
|
| 76 | 79 |
except KeyboardInterrupt:
|
| 77 | 80 |
pass
|
| ... | ... | @@ -105,7 +108,8 @@ def _work_buildbox(context, remote, port, server_cert, client_key, client_cert, |
| 105 | 108 |
context=context,
|
| 106 | 109 |
channel=context.channel,
|
| 107 | 110 |
parent=context.parent,
|
| 108 |
- number_of_leases=context.number_of_leases)
|
|
| 111 |
+ number_of_leases=context.number_of_leases,
|
|
| 112 |
+ continuous=context.continuous)
|
|
| 109 | 113 |
|
| 110 | 114 |
except KeyboardInterrupt:
|
| 111 | 115 |
pass
|
| ... | ... | @@ -39,33 +39,31 @@ class Bot(object): |
| 39 | 39 |
Creates a local BotSession.
|
| 40 | 40 |
"""
|
| 41 | 41 |
|
| 42 |
- def __init__(self, work, context, channel, parent, number_of_leases):
|
|
| 42 |
+ def __init__(self, work, context, channel, parent, number_of_leases, continuous=True):
|
|
| 43 | 43 |
if not inspect.iscoroutinefunction(work):
|
| 44 | 44 |
raise BotError("work function must be async")
|
| 45 | 45 |
|
| 46 |
+ print(type(context))
|
|
| 47 |
+ |
|
| 46 | 48 |
self.interface = bot_interface.BotInterface(channel)
|
| 47 | 49 |
self.logger = logging.getLogger(__name__)
|
| 48 | 50 |
|
| 49 | 51 |
self._create_session(parent, number_of_leases)
|
| 50 | 52 |
self._work_queue = queue.Queue(maxsize = number_of_leases)
|
| 51 | 53 |
|
| 52 |
- try:
|
|
| 53 |
- while True:
|
|
| 54 |
- ## TODO: Leases independently finish
|
|
| 55 |
- ## Allow leases to queue finished work independently instead
|
|
| 56 |
- ## of waiting for all to finish
|
|
| 57 |
- futures = [self._do_work(work, context, lease) for lease in self._get_work()]
|
|
| 58 |
- if futures:
|
|
| 59 |
- loop = asyncio.new_event_loop()
|
|
| 60 |
- leases_complete, _ = loop.run_until_complete(asyncio.wait(futures))
|
|
| 61 |
- work_complete = [(lease.result().assignment, lease.result(),) for lease in leases_complete]
|
|
| 62 |
- self._work_complete(work_complete)
|
|
| 63 |
- loop.close()
|
|
| 64 |
- self._update_bot_session()
|
|
| 65 |
- time.sleep(2)
|
|
| 66 |
- except Exception as e:
|
|
| 67 |
- self.logger.error(e)
|
|
| 68 |
- raise BotError(e)
|
|
| 54 |
+ while continuous:
|
|
| 55 |
+ ## TODO: Leases independently finish
|
|
| 56 |
+ ## Allow leases to queue finished work independently instead
|
|
| 57 |
+ ## of waiting for all to finish
|
|
| 58 |
+ futures = [self._do_work(work, context, lease) for lease in self._get_work()]
|
|
| 59 |
+ if futures:
|
|
| 60 |
+ loop = asyncio.new_event_loop()
|
|
| 61 |
+ leases_complete, _ = loop.run_until_complete(asyncio.wait(futures))
|
|
| 62 |
+ work_complete = [(lease.result().assignment, lease.result(),) for lease in leases_complete]
|
|
| 63 |
+ self._work_complete(work_complete)
|
|
| 64 |
+ loop.close()
|
|
| 65 |
+ self._update_bot_session()
|
|
| 66 |
+ time.sleep(2)
|
|
| 69 | 67 |
|
| 70 | 68 |
@property
|
| 71 | 69 |
def bot_session(self):
|
| ... | ... | @@ -97,7 +95,6 @@ class Bot(object): |
| 97 | 95 |
def _get_work(self):
|
| 98 | 96 |
while not self._work_queue.empty():
|
| 99 | 97 |
yield self._work_queue.get()
|
| 100 |
- self._work_queue.task_done()
|
|
| 101 | 98 |
|
| 102 | 99 |
def _work_complete(self, leases_complete):
|
| 103 | 100 |
""" Bot updates itself with any completed work.
|
| ... | ... | @@ -32,14 +32,14 @@ class DiskStorage(StorageABC): |
| 32 | 32 |
|
| 33 | 33 |
def __init__(self, path):
|
| 34 | 34 |
self._path = pathlib.Path(path)
|
| 35 |
- os.makedirs(self._path / "temp", exist_ok=True)
|
|
| 35 |
+ os.makedirs(str(self._path / "temp"), exist_ok=True)
|
|
| 36 | 36 |
|
| 37 | 37 |
def has_blob(self, digest):
|
| 38 | 38 |
return (self._path / (digest.hash + "_" + str(digest.size_bytes))).exists()
|
| 39 | 39 |
|
| 40 | 40 |
def get_blob(self, digest):
|
| 41 | 41 |
try:
|
| 42 |
- return open(self._path / (digest.hash + "_" + str(digest.size_bytes)), 'rb')
|
|
| 42 |
+ return (self._path / (digest.hash + "_" + str(digest.size_bytes))).open('rb')
|
|
| 43 | 43 |
except FileNotFoundError:
|
| 44 | 44 |
return None
|
| 45 | 45 |
|
| ... | ... | @@ -49,7 +49,7 @@ class DiskStorage(StorageABC): |
| 49 | 49 |
def commit_write(self, digest, write_session):
|
| 50 | 50 |
# Atomically move the temporary file into place.
|
| 51 | 51 |
path = self._path / (digest.hash + "_" + str(digest.size_bytes))
|
| 52 |
- os.replace(write_session.name, path)
|
|
| 52 |
+ os.replace(write_session.name, str(path))
|
|
| 53 | 53 |
try:
|
| 54 | 54 |
write_session.close()
|
| 55 | 55 |
except FileNotFoundError:
|
| ... | ... | @@ -83,7 +83,7 @@ def test_bytestream_read(data_to_read, instance): |
| 83 | 83 |
request = bytestream_pb2.ReadRequest()
|
| 84 | 84 |
if instance != "":
|
| 85 | 85 |
request.resource_name = instance + "/"
|
| 86 |
- request.resource_name += f"blobs/{HASH(data_to_read).hexdigest()}/{len(data_to_read)}"
|
|
| 86 |
+ request.resource_name += "blobs/{}/{}".format(HASH(data_to_read).hexdigest(), len(data_to_read))
|
|
| 87 | 87 |
|
| 88 | 88 |
data = b""
|
| 89 | 89 |
for response in servicer.Read(request, None):
|
| ... | ... | @@ -101,7 +101,7 @@ def test_bytestream_read_many(instance): |
| 101 | 101 |
request = bytestream_pb2.ReadRequest()
|
| 102 | 102 |
if instance != "":
|
| 103 | 103 |
request.resource_name = instance + "/"
|
| 104 |
- request.resource_name += f"blobs/{HASH(data_to_read).hexdigest()}/{len(data_to_read)}"
|
|
| 104 |
+ request.resource_name += "blobs/{}/{}".format(HASH(data_to_read).hexdigest(), len(data_to_read))
|
|
| 105 | 105 |
|
| 106 | 106 |
data = b""
|
| 107 | 107 |
for response in servicer.Read(request, None):
|
| ... | ... | @@ -119,7 +119,7 @@ def test_bytestream_write(instance, extra_data): |
| 119 | 119 |
if instance != "":
|
| 120 | 120 |
resource_name = instance + "/"
|
| 121 | 121 |
hash_ = HASH(b'abcdef').hexdigest()
|
| 122 |
- resource_name += f"uploads/UUID-HERE/blobs/{hash_}/6"
|
|
| 122 |
+ resource_name += "uploads/UUID-HERE/blobs/{}/6".format(hash_)
|
|
| 123 | 123 |
resource_name += extra_data
|
| 124 | 124 |
requests = [
|
| 125 | 125 |
bytestream_pb2.WriteRequest(resource_name=resource_name, data=b'abc'),
|
| ... | ... | @@ -139,7 +139,7 @@ def test_bytestream_write_rejects_wrong_hash(): |
| 139 | 139 |
|
| 140 | 140 |
data = b'some data'
|
| 141 | 141 |
wrong_hash = HASH(b'incorrect').hexdigest()
|
| 142 |
- resource_name = f"uploads/UUID-HERE/blobs/{wrong_hash}/9"
|
|
| 142 |
+ resource_name = "uploads/UUID-HERE/blobs/{}/9".format(wrong_hash)
|
|
| 143 | 143 |
requests = [
|
| 144 | 144 |
bytestream_pb2.WriteRequest(resource_name=resource_name, data=data, finish_write=True)
|
| 145 | 145 |
]
|
| 1 |
+# Copyright (C) 2018 Codethink Limited
|
|
| 2 |
+#
|
|
| 3 |
+# Licensed under the Apache License, Version 2.0 (the "License");
|
|
| 4 |
+# you may not use this file except in compliance with the License.
|
|
| 5 |
+# You may obtain a copy of the License at
|
|
| 6 |
+#
|
|
| 7 |
+# <http://www.apache.org/licenses/LICENSE-2.0>
|
|
| 8 |
+#
|
|
| 9 |
+# Unless required by applicable law or agreed to in writing, software
|
|
| 10 |
+# distributed under the License is distributed on an "AS IS" BASIS,
|
|
| 11 |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
| 12 |
+# See the License for the specific language governing permissions and
|
|
| 13 |
+# limitations under the License.
|
|
| 14 |
+#
|
|
| 15 |
+# Authors:
|
|
| 16 |
+# Finn Ball <finn ball codethink co uk>
|
|
| 17 |
+ |
|
| 18 |
+import copy
|
|
| 19 |
+import grpc
|
|
| 20 |
+import logging
|
|
| 21 |
+import mock
|
|
| 22 |
+import pytest
|
|
| 23 |
+import uuid
|
|
| 24 |
+ |
|
| 25 |
+from buildgrid.bot import bot, bot_interface
|
|
| 26 |
+ |
|
| 27 |
+async def _work_dummy(context, lease):
|
|
| 28 |
+ return lease
|
|
| 29 |
+ |
|
| 30 |
+class ContextMock():
|
|
| 31 |
+ def __init__(self):
|
|
| 32 |
+ self.logger = logging.getLogger(__name__)
|
|
| 33 |
+ |
|
| 34 |
+# GRPC context
|
|
| 35 |
+@pytest.fixture
|
|
| 36 |
+def context():
|
|
| 37 |
+ yield ContextMock()
|
|
| 38 |
+ |
|
| 39 |
+# GRPC context
|
|
| 40 |
+@pytest.fixture
|
|
| 41 |
+def channel():
|
|
| 42 |
+ yield mock.MagicMock(spec = grpc.insecure_channel(''))
|
|
| 43 |
+ |
|
| 44 |
+@pytest.fixture
|
|
| 45 |
+def instance(channel):
|
|
| 46 |
+ yield bot.Bot(work=_work_dummy,
|
|
| 47 |
+ context=ContextMock(),
|
|
| 48 |
+ channel=channel,
|
|
| 49 |
+ parent='rach',
|
|
| 50 |
+ number_of_leases=1,
|
|
| 51 |
+ continuous=False)
|
|
| 52 |
+ |
|
| 53 |
+def test_create_job(instance):
|
|
| 54 |
+ instance.bot_session()
|
