This commit enables the parametrization for "legacy-combined" protocol. All failing tests with that have been either fixed or disabled. Notably: - A couple of tests (e.g. involving builders) could be made to work with a non-local store, but this would require some refactoring to testlib in order to make certain configuration settings generic over local and remote operations. We've disabled those for now, in order to make progress - The store tests all run locally only because of their nature - The flakes tests also all run locally only because my energy for fixing them is limited (and it's a *lot* of test failures, with probably little overall benefit in terms of test coverage) Change-Id: I56fa249a64f7c17c952f688ec89e9687f2a13f12
150 lines
5.3 KiB
Python
150 lines
5.3 KiB
Python
from pathlib import Path
|
|
import sqlite3
|
|
|
|
import aiohttp.web as web
|
|
import pytest
|
|
|
|
from testlib.fixtures.file_helper import File, with_files
|
|
from testlib.fixtures.http_server import http_server
|
|
from testlib.fixtures.nix import Nix, with_diverted_store
|
|
|
|
pytestmark = pytest.mark.no_daemon
|
|
|
|
|
|
class HTTPStore:
|
|
def __init__(self):
|
|
self.uploaded_nars = {}
|
|
self.known_nar_hashes = set()
|
|
|
|
async def upload_narinfo(self, req: web.Request) -> web.Response:
|
|
self.uploaded_nars[req.match_info["hash"]] = self._parse_narinfo(await req.text())
|
|
return web.Response(text="")
|
|
|
|
async def upload_nar(self, req: web.Request) -> web.Response:
|
|
self.known_nar_hashes.add(req.match_info["narhash"])
|
|
return web.Response(text="")
|
|
|
|
async def nix_cache_info(self, _: web.Request) -> web.Response:
|
|
return web.Response(text="StoreDir: /nix/store")
|
|
|
|
async def get_narinfo(self, req: web.Request) -> web.Response:
|
|
narinfo_hash = req.match_info["hash"]
|
|
if narinfo_hash not in self.uploaded_nars:
|
|
return web.Response(text="", status=404)
|
|
return web.Response(
|
|
text="\n".join(f"{k}: {v}" for k, v in self.uploaded_nars[narinfo_hash].items()) + "\n"
|
|
)
|
|
|
|
async def nar_head(self, req: web.Request) -> web.Response:
|
|
return web.Response(
|
|
text="", status=200 if req.match_info["narhash"] in self.known_nar_hashes else 404
|
|
)
|
|
|
|
def _parse_narinfo(self, text: str) -> dict[str, str]:
|
|
narinfo = {}
|
|
for line in text.splitlines():
|
|
key, value = line.split(": ", 1)
|
|
narinfo[key] = value
|
|
_, hashpart = narinfo["FileHash"].split(":", 1)
|
|
assert hashpart in self.known_nar_hashes
|
|
return narinfo
|
|
|
|
|
|
class FakeNARBridge(HTTPStore):
|
|
"""
|
|
HTTP Store that mutates the URL field of the narinfo, just like
|
|
nar-bridge from snix.dev.
|
|
|
|
Testcase to ensure that the correct URL to the nar (i.e. nar/snix-castore/...)
|
|
ends up in the disk-cache.
|
|
"""
|
|
|
|
async def upload_narinfo(self, req: web.Request) -> web.Response:
|
|
narinfo = self._parse_narinfo(await req.text())
|
|
narinfo["URL"] = (
|
|
f"nar/snix-castore/00000000000000000000000000000000000000000000000000000?narsize=f{narinfo['FileSize']}"
|
|
)
|
|
|
|
self.uploaded_nars[req.match_info["hash"]] = narinfo
|
|
return web.Response(text="")
|
|
|
|
|
|
@pytest.fixture(params=[HTTPStore, FakeNARBridge])
|
|
def store(request: pytest.FixtureRequest) -> HTTPStore:
|
|
store_class = request.param
|
|
return store_class()
|
|
|
|
|
|
def start_server(store: HTTPStore) -> web.Application:
|
|
app = web.Application()
|
|
app.add_routes(
|
|
[
|
|
web.put("/{hash}.narinfo", store.upload_narinfo),
|
|
web.get("/{hash}.narinfo", store.get_narinfo),
|
|
web.put("/nar/{narhash}.nar", store.upload_nar),
|
|
web.get("/nix-cache-info", store.nix_cache_info),
|
|
web.head("/nar/{narhash}.nar", store.nar_head),
|
|
]
|
|
)
|
|
|
|
return app
|
|
|
|
|
|
def nars_from_narinfo_cache(db_path: Path) -> list[dict[str, str | bool]]:
|
|
assert db_path.exists()
|
|
db = sqlite3.connect(db_path)
|
|
rows = db.execute(
|
|
"SELECT n.present, n.hashPart, n.namePart, n.url FROM NARs n INNER JOIN BinaryCaches b ON n.cache = b.id WHERE b.url LIKE '%localhost%'"
|
|
)
|
|
return [
|
|
{"present": bool(present), "hashPart": hashPart, "namePart": namePart, "url": url}
|
|
for present, hashPart, namePart, url in rows
|
|
]
|
|
|
|
|
|
@with_files({"test-file": File("hello world")})
|
|
@with_diverted_store
|
|
def test_http_simple(nix: Nix, store: HTTPStore, files: Path):
|
|
test_file = files / "test-file"
|
|
result = nix.nix(cmd=["store", "add-file", test_file], flake=True).run()
|
|
result.ok()
|
|
store_path = result.stdout_plain
|
|
hash_part, _ = Path(store_path).stem.split("-", 1)
|
|
|
|
nar_info_cache = nix.env.dirs.xdg_cache_home / "nix" / "binary-cache-v6.sqlite"
|
|
|
|
app = start_server(store)
|
|
with http_server(app) as httpd:
|
|
url = f"http://localhost:{httpd.port}?compression=none&store=/nix/store"
|
|
nix.nix(cmd=["store", "ping", "--store", url], flake=True).run().ok()
|
|
|
|
# Narinfo shouldn't exist yet
|
|
nix.nix(cmd=["path-info", "--store", url, store_path], flake=True).run().expect(1)
|
|
cache_entries = nars_from_narinfo_cache(nar_info_cache)
|
|
|
|
assert len(cache_entries) == 1
|
|
assert not cache_entries[0]["present"]
|
|
assert cache_entries[0]["hashPart"] == hash_part
|
|
|
|
# Successful upload
|
|
nix.nix(
|
|
cmd=["copy", "--from", nix.settings.store, "--to", url, store_path], flake=True
|
|
).run().ok()
|
|
assert hash_part in store.uploaded_nars
|
|
|
|
# Make sure the negative entry got removed
|
|
cache_entries = nars_from_narinfo_cache(nar_info_cache)
|
|
assert len(cache_entries) == 0
|
|
|
|
# Ensure that the narinfo can be found now.
|
|
nix.nix(cmd=["path-info", "--store", url, store_path], flake=True).run().ok()
|
|
|
|
# Ensure local narinfo cache is up-to-date.
|
|
nar_entries = nars_from_narinfo_cache(nar_info_cache)
|
|
assert len(nar_entries) == 1
|
|
|
|
assert nar_entries[0]["present"]
|
|
assert nar_entries[0]["hashPart"] == hash_part
|
|
assert nar_entries[0]["namePart"] == "test-file"
|
|
assert nar_entries[0]["url"] == store.uploaded_nars[hash_part]["URL"]
|