summaryrefslogtreecommitdiffstats
path: root/tests/e2e/conftest.py
blob: 8159824d10a5c61150038a3264d4d73033dc73a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# 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
    )