blob: 63baf602b4fc33a70850b564bbd28b9ee8e3c8f3 [file]
# 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")