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