blob: 67c3d53afdc02c9a864cd3ab707a2086bc6c00fe [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.
"""E2E tests for the core `repo init` & `repo sync` options."""
from pathlib import Path
from typing import List, Set
from harness import GIT_DEFAULTS_PARAMS
from harness import GitHost
from harness import manifest_xml
from harness import move_aside
from harness import RepoClient
import pytest
@pytest.mark.parametrize("git_defaults", GIT_DEFAULTS_PARAMS)
def test_init_sync_basic(host: GitHost, client: RepoClient) -> None:
"""The basic `repo init && repo sync` flow."""
foo = host.create_repo("platform/foo", {"foo.txt": "foo"})
host.create_repo("platform/bar", {"bar.txt": "bar"})
host.create_manifest(
manifest_xml(
'<project name="platform/foo" path="foo" />',
'<project name="platform/bar" path="sub/bar" />',
)
)
client.init(host.manifest_url)
client.sync()
assert (client.top / "foo" / "foo.txt").read_text() == "foo"
assert (client.top / "sub" / "bar" / "bar.txt").read_text() == "bar"
assert client.head("foo") == foo.rev_parse("main")
assert client.list_projects() == {
"foo": "platform/foo",
"sub/bar": "platform/bar",
}
# The code under test is what got installed & is running.
assert "v99.0-e2e" in client.run("version").stdout
@pytest.mark.parametrize(
"init_args,sync_args,manifest_refs,project_refs",
[
# `init -c` is the default for the manifest project.
([], [], {"main"}, {"main", "other"}),
(["--no-current-branch"], [], {"main", "other"}, {"main", "other"}),
([], ["-c"], {"main"}, {"main"}),
],
)
def test_current_branch(
host: GitHost,
client: RepoClient,
init_args: List[str],
sync_args: List[str],
manifest_refs: Set[str],
project_refs: Set[str],
) -> None:
"""`init` limits the manifest fetch; `sync -c` limits projects."""
foo = host.create_repo("platform/foo", {"foo.txt": "main"})
foo.commit({"foo.txt": "other"}, branch="other")
manifest = host.create_manifest(
manifest_xml('<project name="platform/foo" />')
)
manifest.commit({"README": "other"}, branch="other")
client.init(host.manifest_url, *init_args)
assert client.remote_branches(".repo/manifests") == manifest_refs
client.sync(*sync_args)
assert client.remote_branches("platform/foo") == project_refs
assert (client.top / "platform/foo/foo.txt").read_text() == "main"
@pytest.mark.parametrize(
"sync_args,expect_bundle", [([], True), (["--no-clone-bundle"], False)]
)
def test_clone_bundle(
host: GitHost,
client: RepoClient,
sync_args: List[str],
expect_bundle: bool,
) -> None:
"""repo bootstraps new projects from clone.bundle when available."""
foo = host.create_repo("platform/foo", {"foo.txt": "foo"})
# A commit that only the bundle has, to prove the bundle was applied.
bundle_only = foo.commit({"x": "bundle"}, branch="bundle-only")
foo.write_bundle()
foo.git("update-ref", "-d", "refs/heads/bundle-only")
host.create_manifest(manifest_xml('<project name="platform/foo" />'))
client.init(host.manifest_url)
client.sync(*sync_args)
bundle_gets = [
r
for r in host.requests()
if r.path.endswith("/platform/foo/clone.bundle")
]
assert [r.status for r in bundle_gets] == ([200] if expect_bundle else [])
assert client.has_object("platform/foo", bundle_only) == expect_bundle
assert client.head("platform/foo") == foo.rev_parse("main")
@pytest.mark.parametrize(
"init_args,project_attrs",
[
(["--depth=1"], ""),
([], 'clone-depth="1"'),
],
)
def test_shallow(
host: GitHost,
client: RepoClient,
init_args: List[str],
project_attrs: str,
) -> None:
"""Shallow clones via `--depth` and manifest `clone-depth`."""
foo = host.create_repo("platform/foo", {"foo.txt": "1"})
foo.commit({"foo.txt": "2"})
foo.commit({"foo.txt": "3"})
host.create_manifest(
manifest_xml(f'<project name="platform/foo" {project_attrs} />')
)
client.init(host.manifest_url, *init_args)
client.sync()
path = "platform/foo"
assert client.is_shallow(path)
assert client.git(path, "rev-list", "--count", "HEAD") == "1"
assert (client.top / path / "foo.txt").read_text() == "3"
@pytest.mark.parametrize(
"init_args,expected",
[
([], {"a", "b"}),
(["-g", "all"], {"a", "b", "c", "d"}),
(["-g", "g2"], {"c"}),
(["-g", "default,-g1"], {"a"}),
(["-g", "default,g2"], {"a", "b", "c"}),
(["-g", "name:platform/d"], {"d"}),
],
)
def test_groups(
host: GitHost, client: RepoClient, init_args: List[str], expected: Set[str]
) -> None:
"""Manifest groups select which projects get synced."""
for name in "abcd":
host.create_repo(f"platform/{name}", {"f": name})
host.create_manifest(
manifest_xml(
'<project name="platform/a" path="a" />',
'<project name="platform/b" path="b" groups="g1" />',
'<project name="platform/c" path="c" groups="g2,notdefault" />',
'<project name="platform/d" path="d" groups="notdefault" />',
)
)
client.init(host.manifest_url, *init_args)
client.sync()
assert set(client.list_projects()) == expected
assert client.checkouts() == expected
def test_local_manifests(host: GitHost, client: RepoClient) -> None:
""".repo/local_manifests/ can add, remove & extend projects."""
foo = host.create_repo("platform/foo", {"f": "main"})
stable = foo.commit({"f": "stable"}, branch="stable")
host.create_repo("platform/bar", {"f": "bar"})
host.create_repo("vendor/extra", {"f": "extra"})
host.create_manifest(
manifest_xml(
'<project name="platform/foo" path="foo" />',
'<project name="platform/bar" path="bar" />',
)
)
client.init(host.manifest_url)
local = client.repodir / "local_manifests"
local.mkdir()
(local / "local.xml").write_text(
"""<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remove-project name="platform/bar" />
<extend-project name="platform/foo" revision="stable" />
<project name="vendor/extra" path="extra" />
</manifest>
"""
)
client.sync()
assert client.list_projects() == {
"extra": "vendor/extra",
"foo": "platform/foo",
}
assert client.head("foo") == stable
assert not (client.top / "bar").exists()
def test_manifest_name(host: GitHost, client: RepoClient) -> None:
"""`-m` selects a manifest file other than default.xml."""
host.create_repo("platform/foo", {"f": "foo"})
host.create_repo("platform/bar", {"f": "bar"})
host.create_manifest(
{
"default.xml": manifest_xml('<project name="platform/foo" />'),
"other.xml": manifest_xml('<project name="platform/bar" />'),
}
)
client.init(host.manifest_url, "-m", "other.xml")
client.sync()
assert client.list_projects() == {"platform/bar": "platform/bar"}
@pytest.mark.parametrize("dissociate", [False, True])
def test_reference_mirror(
host: GitHost, client: RepoClient, dissociate: bool
) -> None:
"""`init --reference` borrows objects from a local `--mirror` checkout.
With `--dissociate` the objects are copied so the mirror can go away.
"""
foo = host.create_repo("platform/foo", {"f": "foo"})
host.create_manifest(manifest_xml('<project name="platform/foo" />'))
mirror = RepoClient(
client.top.parent / "mirror",
client.launcher,
client.env,
client.repo_url,
)
mirror.init(host.manifest_url, "--mirror")
mirror.sync()
if dissociate:
# Dissociating the manifest repo itself currently fails (its
# preciousObjects setting blocks `git repack -a -d`), so only reference
# the mirror's projects.
move_aside(mirror.repodir / "manifests.git")
init_args = ["--reference", str(mirror.top)]
if dissociate:
init_args.append("--dissociate")
client.init(host.manifest_url, *init_args)
client.sync()
path = "platform/foo"
assert client.head(path) == foo.rev_parse("main")
assert (client.top / path / "f").read_text() == "foo"
assert client.has_object(path, foo.rev_parse("main"))
objdir = client.repodir / "project-objects/platform/foo.git"
alternates = objdir / "objects/info/alternates"
assert client.config(".repo/manifests", "repo.reference") == str(mirror.top)
if dissociate:
assert not alternates.exists()
assert client.config(".repo/manifests", "repo.dissociate") == "true"
else:
expected = mirror.top / "platform/foo.git/objects"
assert Path(alternates.read_text().strip()) == expected
# Nothing had to be downloaded or copied into the local object dir.
counts = client.git(".", f"--git-dir={objdir}", "count-objects", "-v")
assert "count: 0\n" in counts
assert "in-pack: 0\n" in counts
# Updates still come from the server (the mirror isn't synced).
latest = foo.commit({"f": "2"})
client.sync()
assert client.head(path) == latest
# If the mirror goes away, `--dissociate` checkouts still have all
# historical objects, whereas shared checkouts lost the borrowed ones.
move_aside(mirror.top)
if dissociate:
assert client.git(path, "rev-list", "--count", "HEAD") == "2"
client.git(path, "rev-list", "--objects", "HEAD")
else:
with pytest.raises(AssertionError, match="rev-list"):
client.git(path, "rev-list", "--objects", "HEAD")
def test_standalone_manifest(host: GitHost, client: RepoClient) -> None:
"""`init --standalone-manifest` fetches a static manifest file over HTTP.
There's no manifest checkout to update, so syncs only fetch the projects.
"""
foo = host.create_repo("platform/foo", {"f": "1"})
host.create_repo("platform/bar", {"f": "bar"})
xml = manifest_xml('<project name="platform/foo" />', fetch=host.url)
manifest_url = host.publish_file("default.xml", xml)
client.init(manifest_url, "--standalone-manifest")
client.sync()
path = "platform/foo"
assert client.head(path) == foo.rev_parse("main")
assert (client.repodir / "manifests/default.xml").read_text() == xml
assert (
client.config(".repo/manifests.git", "manifest.standalone")
== manifest_url
)
# Updating the static file on the server does not affect `repo sync`,
# which only fetches the project.
host.publish_file(
"default.xml",
manifest_xml(
'<project name="platform/foo" />',
'<project name="platform/bar" />',
fetch=host.url,
),
)
latest = foo.commit({"f": "2"})
with host.watch_requests() as requests:
client.sync()
assert client.head(path) == latest
assert client.list_projects() == {"platform/foo": "platform/foo"}
assert requests == host.requests_for("platform/foo", requests)
# Re-initializing without a manifest URL isn't allowed.
result = client.run("init", "--no-repo-verify", check=False)
assert result.returncode != 0
assert "cannot be re-initialized without --manifest-url/-u" in result.stderr
def test_manifest_depth(host: GitHost, client: RepoClient) -> None:
"""`init --manifest-depth` shallow-clones the manifest repo only."""
foo = host.create_repo("platform/foo", {"f": "1"})
foo.commit({"f": "2"})
host.create_manifest(manifest_xml())
host.update_manifest(manifest_xml('<project name="platform/foo" />'))
client.init(host.manifest_url, "--manifest-depth=1")
manifests = ".repo/manifests"
assert client.is_shallow(manifests)
assert client.git(manifests, "rev-list", "--count", "HEAD") == "1"
# `--nmu` skips updating the manifest project, keeping it shallow while
# syncing projects at full depth.
client.sync("--nmu")
assert client.is_shallow(manifests)
assert not client.is_shallow("platform/foo")
assert client.git("platform/foo", "rev-list", "--count", "HEAD") == "2"