| # 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. | |
| """Fixtures for the hermetic end-to-end tests. | |
| Tests normally only need the `host` fixture (to create server repos) and the | |
| `client` fixture (to run repo). Override the `git_defaults` fixture via | |
| parametrize to run under Git 3 defaults. | |
| """ | |
| import itertools | |
| from pathlib import Path | |
| from typing import Dict, Iterator, List | |
| import harness | |
| import pytest | |
| # Run before pytest's own `-m` deselection so `-m e2e` & `-m 'not e2e'` see the | |
| # marker. | |
| @pytest.hookimpl(tryfirst=True) | |
| def pytest_collection_modifyitems(items: List[pytest.Item]) -> None: | |
| """Mark tests in this directory as `e2e` & run them after unit tests. | |
| Note: pytest passes every collected item (not just this directory's), so | |
| this reorders the whole session. | |
| """ | |
| this_dir = Path(__file__).resolve().parent | |
| for item in items: | |
| if this_dir in item.path.resolve().parents: | |
| item.add_marker(pytest.mark.e2e) | |
| # Stable sort: unit tests keep their order & come first, then E2E tests. | |
| items.sort(key=lambda item: item.get_closest_marker("e2e") is not None) | |
| @pytest.fixture(scope="session", autouse=True) | |
| def _require_e2e_support() -> None: | |
| """Skip the suite when the host can't run it.""" | |
| # The code under test is published from the git checkout (e.g. this isn't | |
| # the case when running from an unpacked sdist). | |
| if not (harness.ROOT_DIR / ".git").exists(): | |
| pytest.skip("E2E tests must run from a git checkout") | |
| if not harness.has_http_backend(): | |
| pytest.skip("git http-backend is not available") | |
| if harness.git_version() < (2, 28, 0): | |
| pytest.skip("E2E tests require git 2.28+") | |
| @pytest.fixture(scope="session") | |
| def git_server( | |
| tmp_path_factory: pytest.TempPathFactory, | |
| ) -> Iterator[harness.GitHttpServer]: | |
| """A git smart HTTP server shared by all tests in the session.""" | |
| with harness.serve(tmp_path_factory.mktemp("git-server")) as server: | |
| yield server | |
| @pytest.fixture(scope="session") | |
| def launcher(tmp_path_factory: pytest.TempPathFactory) -> Path: | |
| """A standalone copy of the repo launcher.""" | |
| return harness.install_launcher(tmp_path_factory.mktemp("bin")) | |
| @pytest.fixture(scope="session") | |
| def repo_source(git_server: harness.GitHttpServer) -> harness.ServerRepo: | |
| """The code under test, published as a repo on `git_server`.""" | |
| return harness.publish_repo_source( | |
| git_server.root / "git-repo.git", f"{git_server.url}/git-repo" | |
| ) | |
| NAMESPACE_IDS = itertools.count() | |
| @pytest.fixture | |
| def host(git_server: harness.GitHttpServer) -> harness.GitHost: | |
| """A per-test namespace for creating repos on the server.""" | |
| return harness.GitHost(git_server, f"t{next(NAMESPACE_IDS)}") | |
| @pytest.fixture | |
| def git_defaults() -> str: | |
| """Client git defaults to emulate; a key of harness.GIT_DEFAULTS.""" | |
| return "git2" | |
| @pytest.fixture | |
| def client_env(tmp_path: Path, git_defaults: str) -> Dict[str, str]: | |
| """Isolated environment for running repo & git as a client.""" | |
| home = tmp_path / "home" | |
| home.mkdir() | |
| settings = { | |
| "user.name": "E2E User", | |
| "user.email": "user@example.com", | |
| "color.ui": "false", | |
| # Don't let background maintenance race with assertions. | |
| "maintenance.auto": "false", | |
| "gc.auto": "0", | |
| } | |
| if git_defaults == "git3" and harness.git_version() < (2, 47, 0): | |
| # init.defaultRefFormat & init.defaultObjectFormat landed in git 2.47. | |
| pytest.skip("git is too old to emulate Git 3 defaults") | |
| settings.update(harness.GIT_DEFAULTS[git_defaults]) | |
| harness.write_config(home / ".gitconfig", settings) | |
| # Python resolves `~` via USERPROFILE on Windows. The ceiling keeps git | |
| # from finding a repo above the test dir (e.g. with `--basetemp=./tmp`). | |
| return dict( | |
| harness.clean_environ(), | |
| HOME=str(home), | |
| USERPROFILE=str(home), | |
| GIT_CEILING_DIRECTORIES=str(tmp_path), | |
| ) | |
| @pytest.fixture | |
| def client( | |
| tmp_path: Path, | |
| launcher: Path, | |
| repo_source: harness.ServerRepo, | |
| client_env: Dict[str, str], | |
| ) -> harness.RepoClient: | |
| """A repo client checkout directory that uses the code under test.""" | |
| return harness.RepoClient( | |
| tmp_path / "client", launcher, client_env, repo_source.url | |
| ) |