mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-11-14 16:27:13 +00:00
Cover `--reference` from a local mirror (with & without `--dissociate`), `--standalone-manifest` with a manifest file served over HTTP, and `--manifest-depth`. Teach the server to serve static files (`GitHost.publish_file`), let `manifest_xml` take an absolute `fetch` URL, and add `move_aside` to make git dirs disappear mid-test (deleting them fails on Windows). Bug: 566270944 Change-Id: Id60a19613a627f76d8d9125570dd5ce0cdfff9aa Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/648549 Reviewed-by: Gavin Mak <gavinmak@google.com> Tested-by: Brian Gan <brgan@google.com> Commit-Queue: Brian Gan <brgan@google.com>
210 lines
7.8 KiB
Python
210 lines
7.8 KiB
Python
# Copyright (C) 2026 The Android Open Source Project
|
|
#
|
|
# 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.
|
|
|
|
"""Tests for the E2E harness helpers themselves."""
|
|
|
|
import http.client
|
|
from pathlib import Path
|
|
from typing import Mapping, Tuple
|
|
import urllib.parse
|
|
|
|
import harness
|
|
import pytest
|
|
|
|
|
|
def _http_request(
|
|
server: harness.GitHttpServer, method: str, path: str
|
|
) -> Tuple[int, bytes]:
|
|
"""Send a request to `server` & return the response status & body."""
|
|
url = urllib.parse.urlsplit(server.url)
|
|
conn = http.client.HTTPConnection(url.hostname, url.port, timeout=30)
|
|
try:
|
|
conn.request(method, path)
|
|
resp = conn.getresponse()
|
|
return resp.status, resp.read()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"key,value",
|
|
[
|
|
("core.bare", "true"),
|
|
('url.C:\\src\\#1;"x".insteadOf', 'C:\\dst\\ #2;"y"'),
|
|
("alias.multi", "a\nb"),
|
|
],
|
|
)
|
|
def test_write_config(tmp_path: Path, key: str, value: str) -> None:
|
|
"""Subsections & values round-trip through git's config parser."""
|
|
path = tmp_path / "config"
|
|
harness.write_config(path, {key: value})
|
|
got = harness.run_git("config", "--file", str(path), "--get", key)
|
|
assert got == value
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"files",
|
|
[
|
|
{"foo/bar": "x", "foo": None},
|
|
{"foo": None, "foo/bar": "x"},
|
|
],
|
|
)
|
|
def test_commit_replace_file_with_dir(
|
|
tmp_path: Path, files: Mapping[str, harness.FileContent]
|
|
) -> None:
|
|
"""Deletions apply before additions regardless of dict order."""
|
|
repo = harness.ServerRepo.create(tmp_path / "repo.git", "unused")
|
|
repo.commit({"foo": "file"})
|
|
repo.commit(files)
|
|
assert repo.git("ls-tree", "-r", "--name-only", "main") == "foo/bar"
|
|
|
|
|
|
def test_snapshot_worktree_host_excludes(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""The developer's global excludes apply despite the isolated HOME."""
|
|
host_home = tmp_path / "host-home"
|
|
host_home.mkdir()
|
|
excludes = host_home / "excludes"
|
|
excludes.write_text("venv/\n")
|
|
# Set excludesFile explicitly so a system-wide one can't take precedence.
|
|
harness.write_config(
|
|
host_home / ".gitconfig", {"core.excludesFile": excludes.as_posix()}
|
|
)
|
|
monkeypatch.setattr(harness, "HOST_ENVIRON", {"HOME": str(host_home)})
|
|
root = tmp_path / "checkout"
|
|
harness.run_git("init", "--quiet", str(root))
|
|
(root / ".gitignore").write_text("*.pyc\n")
|
|
(root / "main.py").write_text("")
|
|
(root / "main.pyc").write_text("")
|
|
(root / "venv").mkdir()
|
|
(root / "venv" / "lib.py").write_text("")
|
|
assert sorted(harness._snapshot_worktree(root)) == [".gitignore", "main.py"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"chunked,expect_continue",
|
|
[(False, False), (True, False), (True, True)],
|
|
)
|
|
def test_http_post_body(
|
|
git_server: harness.GitHttpServer, chunked: bool, expect_continue: bool
|
|
) -> None:
|
|
"""POST bodies reach git http-backend however curl encodes them."""
|
|
name = f"post-{int(chunked)}{int(expect_continue)}.git"
|
|
repo = harness.ServerRepo.create(git_server.root / name, "unused")
|
|
commit = repo.commit({"file": "x"})
|
|
# A protocol v2 ls-refs request: a command pkt-line & a flush-pkt.
|
|
body = b"0014command=ls-refs\n0000"
|
|
headers = {
|
|
"Content-Type": "application/x-git-upload-pack-request",
|
|
"Git-Protocol": "version=2",
|
|
}
|
|
if expect_continue:
|
|
headers["Expect"] = "100-continue"
|
|
url = urllib.parse.urlsplit(git_server.url)
|
|
conn = http.client.HTTPConnection(url.hostname, url.port, timeout=30)
|
|
try:
|
|
# http.client skips any interim `100 Continue` response.
|
|
conn.request(
|
|
"POST",
|
|
f"/{name}/git-upload-pack",
|
|
body=iter([body[:10], body[10:]]) if chunked else body,
|
|
headers=headers,
|
|
encode_chunked=chunked,
|
|
)
|
|
resp = conn.getresponse()
|
|
assert resp.status == 200
|
|
assert f"{commit} refs/heads/main".encode() in resp.read()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_static_files(git_server: harness.GitHttpServer) -> None:
|
|
"""Published files are served as-is, and nothing outside of them."""
|
|
host = harness.GitHost(git_server, "static-files")
|
|
host.create_repo("repo", {"f": "x"})
|
|
url = host.publish_file("dir/default.xml", "<manifest />\n")
|
|
path = urllib.parse.urlsplit(url).path
|
|
assert _http_request(git_server, "GET", path) == (200, b"<manifest />\n")
|
|
|
|
# Publishing again replaces the file.
|
|
assert host.publish_file("dir/default.xml", b"v2") == url
|
|
assert _http_request(git_server, "GET", path) == (200, b"v2")
|
|
|
|
static = f"/{host.prefix}/{harness.STATIC_DIR}"
|
|
for bad_path in (
|
|
f"{static}/missing.xml",
|
|
f"{static}/dir",
|
|
f"{static}/../repo.git/config",
|
|
f"{static}/%2e%2e/repo.git/config",
|
|
f"{static}//etc/passwd",
|
|
):
|
|
assert _http_request(git_server, "GET", bad_path)[0] == 404, bad_path
|
|
assert _http_request(git_server, "POST", path)[0] == 404
|
|
|
|
|
|
def test_offline(git_server: harness.GitHttpServer) -> None:
|
|
"""Requests fail while a namespace is offline, and work again after."""
|
|
host = harness.GitHost(git_server, "offline")
|
|
repo = host.create_repo("platform/repo", {"f": "x"})
|
|
other = harness.GitHost(git_server, "online")
|
|
other_repo = other.create_repo("repo", {"f": "x"})
|
|
|
|
with host.offline():
|
|
with pytest.raises(AssertionError, match="503"):
|
|
harness.run_git("ls-remote", repo.url)
|
|
# Other namespaces are unaffected.
|
|
assert "refs/heads/main" in harness.run_git("ls-remote", other_repo.url)
|
|
assert {r.status for r in host.requests()} == {503}
|
|
|
|
# The outage ends even if the context exits with an exception.
|
|
with pytest.raises(RuntimeError):
|
|
with host.offline():
|
|
raise RuntimeError("boom")
|
|
assert "refs/heads/main" in harness.run_git("ls-remote", repo.url)
|
|
|
|
|
|
def test_requested_repos(git_server: harness.GitHttpServer) -> None:
|
|
"""`requested_repos()` & `watch_requests()` track repo requests."""
|
|
host = harness.GitHost(git_server, "requested")
|
|
nested = host.create_repo("platform/nested", {"f": "x"})
|
|
top = host.create_repo("top", {"f": "x"})
|
|
host.publish_file("file.xml", "x")
|
|
|
|
harness.run_git("ls-remote", nested.url)
|
|
with host.watch_requests() as watched:
|
|
harness.run_git("ls-remote", top.url)
|
|
_http_request(git_server, "GET", f"/{host.prefix}/top/clone.bundle")
|
|
_http_request(git_server, "GET", f"/{host.prefix}/_static/file.xml")
|
|
|
|
assert host.requested_repos() == {"platform/nested", "top"}
|
|
assert host.requested_repos(watched) == {"top"}
|
|
assert host.requests_for("platform/nested", watched) == []
|
|
|
|
|
|
def test_client_config(tmp_path: Path) -> None:
|
|
"""`RepoClient.config()` tells unset keys apart from errors."""
|
|
# Don't let git find a repo above tmp_path (e.g. with --basetemp).
|
|
env = dict(harness.clean_environ(), GIT_CEILING_DIRECTORIES=str(tmp_path))
|
|
client = harness.RepoClient(tmp_path / "client", harness.LAUNCHER, env, "")
|
|
repo = client.top / "repo"
|
|
harness.run_git("init", "-q", str(repo))
|
|
harness.run_git("config", "-f", str(repo / ".git/config"), "a.key", "val")
|
|
(client.top / "not-a-repo").mkdir()
|
|
|
|
assert client.config("repo", "a.key") == "val"
|
|
assert client.config("repo", "a.unset") is None
|
|
with pytest.raises(AssertionError, match="failed in not-a-repo"):
|
|
client.config("not-a-repo", "a.key")
|