This is an automated email from the ASF dual-hosted git repository. juergbi pushed a commit to branch jbilleter/http-cache in repository https://gitbox.apache.org/repos/asf/buildstream.git
commit 5e5d75493ed5f8dddf39e0be556717d7c66f4025 Author: Jürg Billeter <[email protected]> AuthorDate: Sat Sep 12 17:23:29 2026 +0200 tests/testutils: Add HTTPCASServer --- tests/testutils/__init__.py | 9 ++++- tests/testutils/artifactshare.py | 18 +++++++++ tests/testutils/http_cas_server.py | 79 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/tests/testutils/__init__.py b/tests/testutils/__init__.py index 03eb422ea..4885fefc6 100644 --- a/tests/testutils/__init__.py +++ b/tests/testutils/__init__.py @@ -19,7 +19,14 @@ # William Salmon <[email protected]> # -from .artifactshare import create_artifact_share, create_split_share, assert_shared, assert_not_shared, ArtifactShare +from .artifactshare import ( + create_artifact_share, + create_artifact_and_http_share, + create_split_share, + assert_shared, + assert_not_shared, + ArtifactShare, +) from .casd import casd_cache from .context import dummy_context from .element_generators import create_element_size diff --git a/tests/testutils/artifactshare.py b/tests/testutils/artifactshare.py index fcdbcb597..66958eeb2 100644 --- a/tests/testutils/artifactshare.py +++ b/tests/testutils/artifactshare.py @@ -30,6 +30,8 @@ from buildstream._protos.build.bazel.remote.execution.v2 import remote_execution from buildstream._protos.buildstream.v2 import artifact_pb2 from buildstream._protos.google.rpc import code_pb2 +from .http_cas_server import HTTPCASServer + REMOTE_ASSET_ARTIFACT_URN_TEMPLATE = "urn:fdc:buildstream.build:2020:artifact:{}" REMOTE_ASSET_SOURCE_URN_TEMPLATE = "urn:fdc:buildstream.build:2020:source:{}" @@ -329,6 +331,22 @@ def create_split_share(directory1, directory2, *, quota=None): storage.close() +@contextmanager +def create_artifact_and_http_share(directory, *, quota=None): + share = ArtifactShare(directory, quota=quota) + try: + # Read-only access to the same CAS objects using a HTTP REST protocol + # https://github.com/buchgr/bazel-remote/ + httpserver = HTTPCASServer(share.repodir) + httpserver.start() + try: + yield share, httpserver + finally: + httpserver.stop() + finally: + share.close() + + # create_dummy_artifact_share() # # Create a dummy artifact share that doesn't have any capabilities diff --git a/tests/testutils/http_cas_server.py b/tests/testutils/http_cas_server.py new file mode 100644 index 000000000..7202d2f76 --- /dev/null +++ b/tests/testutils/http_cas_server.py @@ -0,0 +1,79 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import functools +import os +import re +import shutil +import threading +from http.server import BaseHTTPRequestHandler, HTTPStatus, ThreadingHTTPServer + + +class HTTPCASRequestHandler(BaseHTTPRequestHandler): + HASH_SIZE = 64 # SHA256 as hex digits + + def __init__(self, directory, *args, **kwargs): + self.objdir = os.path.join(directory, "cas", "objects") + super().__init__(*args, **kwargs) + + def do_GET(self): + self.process_get_or_head() + + def do_HEAD(self): + self.process_get_or_head() + + def process_get_or_head(self): + match = re.fullmatch(r"/cas/([0-9a-f]+)", self.path) + if match is None: + self.send_error(HTTPStatus.NOT_FOUND) + return + cas_hash = match.group(1) + if len(cas_hash) != self.HASH_SIZE: + self.send_error(HTTPStatus.NOT_FOUND) + return + + try: + with open(os.path.join(self.objdir, cas_hash[:2], cas_hash[2:]), "rb") as f: + fs = os.fstat(f.fileno()) + + self.send_response(HTTPStatus.OK) + self.send_header("Content-Length", str(fs.st_size)) + self.end_headers() + + if self.command == "GET": + shutil.copyfileobj(f, self.wfile) + except OSError: + self.send_error(HTTPStatus.NOT_FOUND) + + +class HTTPCASServer(threading.Thread): + def __init__(self, directory): + super().__init__() + self.server = ThreadingHTTPServer(("127.0.0.1", 0), functools.partial(HTTPCASRequestHandler, directory)) + self.started = False + + def start(self): + self.started = True + super().start() + + def run(self): + self.server.serve_forever() + + def stop(self): + if not self.started: + return + self.server.shutdown() + self.join() + + def base_url(self): + return "http://127.0.0.1:{}".format(self.server.server_port)
