mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-11-15 00:37:13 +00:00
tests: e2e: add tests for init, sync & common workflows
Cover the core `repo init` & `repo sync` options (current branch only, clone bundles, shallow clones, groups, local manifests & manifest names), multi-step workflows (two-phase syncs, mirrors, incremental syncs, local work, switching manifest branches, selfupdate, ...), and manifests that broke users in the past. Some tests also run under Git 3 defaults. Add `RepoClient` helpers to list remote-tracking branches & checkouts, and document the test layout in the README. Bug: 566270944 Change-Id: Iac333216e0f27e489e64f4eb853189f860a91f20 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/648544 Tested-by: Brian Gan <brgan@google.com> Reviewed-by: Gavin Mak <gavinmak@google.com> Commit-Queue: Brian Gan <brgan@google.com>
This commit is contained in:
1 parent
9aac80dafc
commit
dc1811ba23
5 files changed
+901
No files matched your search
@@ -23,3 +23,10 @@ They run as part of `./run_tests` by default, after the unit tests.
|
||||
The suite is skipped automatically when `git http-backend` isn't
|
||||
available, when git is older than 2.28 (need default branch support and
|
||||
superproject support), or when not run from a git checkout.
|
||||
|
||||
## Layout
|
||||
|
||||
* `harness.py`: the git server, server-side repos & the repo client.
|
||||
* `test_init_sync.py`: core `repo init` & `repo sync` options.
|
||||
* `test_workflows.py`: multi-step workflows.
|
||||
* `test_manifest_regressions.py`: manifests that broke users in the past.
|
||||
@@ -49,6 +49,7 @@ from typing import (
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
@@ -934,6 +935,24 @@ class RepoClient:
|
||||
"""Return the commit checked out in `path`."""
|
||||
return self.git(path, "rev-parse", "HEAD")
|
||||
|
||||
def remote_branches(self, path: str) -> Set[str]:
|
||||
"""Return the names of the `origin` remote-tracking branches."""
|
||||
out = self.git(
|
||||
path,
|
||||
"for-each-ref",
|
||||
"--format=%(refname:lstrip=3)",
|
||||
"refs/remotes/origin/",
|
||||
)
|
||||
return set(out.split())
|
||||
|
||||
def checkouts(self) -> Set[str]:
|
||||
"""Return the names of the top-level dirs, except `.repo`."""
|
||||
return {
|
||||
x.name
|
||||
for x in self.top.iterdir()
|
||||
if x.is_dir() and x.name != ".repo"
|
||||
}
|
||||
|
||||
def has_object(self, path: str, oid: str) -> bool:
|
||||
"""Whether the checkout at `path` has object `oid` locally."""
|
||||
# Unlike `cat-file -e`, this never lazily fetches missing objects in
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# 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 typing import List, Set
|
||||
|
||||
from harness import GIT_DEFAULTS_PARAMS
|
||||
from harness import GitHost
|
||||
from harness import manifest_xml
|
||||
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"}
|
||||
@@ -0,0 +1,387 @@
|
||||
# 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 regression tests for manifests that broke users in the past.
|
||||
|
||||
Each test names the commit and/or bug that fixed the original breakage.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from harness import GitHost
|
||||
from harness import Gitlink
|
||||
from harness import manifest_xml
|
||||
from harness import RepoClient
|
||||
import pytest
|
||||
|
||||
|
||||
def test_empty_manifest(host: GitHost, client: RepoClient) -> None:
|
||||
"""A manifest without projects syncs (616e314, b/377546300)."""
|
||||
host.create_manifest(manifest_xml())
|
||||
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
|
||||
assert client.list_projects() == {}
|
||||
|
||||
|
||||
def test_local_extend_project_with_superproject(
|
||||
host: GitHost, client: RepoClient
|
||||
) -> None:
|
||||
"""Local `extend-project` works with superproject overrides.
|
||||
|
||||
Regression test for 3c0e67b (b/470374343).
|
||||
"""
|
||||
foo = host.create_repo("platform/foo", {"f": "v1"})
|
||||
pinned = foo.rev_parse("main")
|
||||
host.create_repo("superproject", {"foo": Gitlink(pinned)})
|
||||
foo.commit({"f": "v2"})
|
||||
host.create_manifest(
|
||||
manifest_xml(
|
||||
'<superproject name="superproject" />',
|
||||
'<project name="platform/foo" path="foo" />',
|
||||
)
|
||||
)
|
||||
client.init(host.manifest_url, "--use-superproject")
|
||||
local = client.repodir / "local_manifests"
|
||||
local.mkdir()
|
||||
(local / "local.xml").write_text(
|
||||
"<manifest>\n"
|
||||
' <extend-project name="platform/foo" groups="extra" />\n'
|
||||
"</manifest>\n"
|
||||
)
|
||||
|
||||
client.sync("--use-superproject")
|
||||
|
||||
assert client.head("foo") == pinned
|
||||
|
||||
|
||||
# 3af9e2f fixed both the regular & the --optimized-fetch skip-fetch checks.
|
||||
@pytest.mark.parametrize("sync_args", [[], ["--optimized-fetch"]])
|
||||
def test_same_project_shallow_pinned_paths(
|
||||
host: GitHost, client: RepoClient, sync_args: List[str]
|
||||
) -> None:
|
||||
"""One project at two paths pinned to SHAs with sync-c, one shallow.
|
||||
|
||||
The non-shallow path shares the shallow path's (depth-limited) object
|
||||
store, so its fetch must not be skipped as already up to date, or its
|
||||
history is left truncated.
|
||||
|
||||
Regression test for 3af9e2f (b/505072873).
|
||||
"""
|
||||
foo = host.create_repo("platform/foo", {"f": "1"})
|
||||
host.create_manifest(manifest_xml())
|
||||
|
||||
def pin(rev: str) -> None:
|
||||
host.update_manifest(
|
||||
manifest_xml(
|
||||
f'<project name="platform/foo" path="a" revision="{rev}" '
|
||||
'clone-depth="1" />',
|
||||
f'<project name="platform/foo" path="b" revision="{rev}" />',
|
||||
default='remote="origin" revision="main" sync-c="true"',
|
||||
)
|
||||
)
|
||||
|
||||
pin(foo.commit({"f": "2"}))
|
||||
client.init(host.manifest_url)
|
||||
client.sync(*sync_args)
|
||||
|
||||
# Skip a commit so the shallow fetch alone would leave a gap in the shared
|
||||
# history.
|
||||
foo.commit({"f": "3"})
|
||||
latest = foo.commit({"f": "4"})
|
||||
pin(latest)
|
||||
client.sync(*sync_args)
|
||||
|
||||
assert client.is_shallow("a")
|
||||
for path in ("a", "b"):
|
||||
assert client.head(path) == latest
|
||||
# Fails to traverse the shared, truncated history if `path`'s fetch
|
||||
# was skipped.
|
||||
client.git(path, "rev-list", "HEAD")
|
||||
|
||||
|
||||
def test_shallow_pinned_with_upstream(
|
||||
host: GitHost, client: RepoClient
|
||||
) -> None:
|
||||
"""clone-depth + upstream + SHA revisions stay shallow.
|
||||
|
||||
The bug only triggered when the upstream branch was ahead of the pinned
|
||||
SHA, so each pin leaves `main` one commit ahead.
|
||||
|
||||
Regression test for d9cc0a1 (b/427093249).
|
||||
"""
|
||||
foo = host.create_repo("platform/foo", {"f": "1"})
|
||||
host.create_manifest(manifest_xml())
|
||||
|
||||
def pin(rev: str) -> None:
|
||||
host.update_manifest(
|
||||
manifest_xml(
|
||||
f'<project name="platform/foo" revision="{rev}" '
|
||||
'upstream="main" clone-depth="1" />'
|
||||
)
|
||||
)
|
||||
|
||||
path = "platform/foo"
|
||||
pinned = foo.commit({"f": "2"})
|
||||
foo.commit({"f": "2 ahead"})
|
||||
pin(pinned)
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
assert client.head(path) == pinned
|
||||
assert client.git(path, "rev-list", "--count", "HEAD") == "1"
|
||||
|
||||
pinned = foo.commit({"f": "3"})
|
||||
foo.commit({"f": "3 ahead"})
|
||||
pin(pinned)
|
||||
client.sync()
|
||||
|
||||
assert client.head(path) == pinned
|
||||
assert client.git(path, "rev-list", "--count", "HEAD") == "1"
|
||||
|
||||
|
||||
def test_pinned_revision_refreshes_upstream(
|
||||
host: GitHost, client: RepoClient
|
||||
) -> None:
|
||||
"""Pinned SHAs already present locally still refresh the upstream ref.
|
||||
|
||||
Regression test for d27d682: when the pinned commit was already in the
|
||||
local object store (e.g. prefetched), `sync -c` skipped the fetch as an
|
||||
immutable revision, so the stale tracking ref of the upstream branch was
|
||||
never updated.
|
||||
"""
|
||||
foo = host.create_repo("platform/foo", {"f": "1"})
|
||||
host.create_manifest(manifest_xml())
|
||||
|
||||
def pin(rev: str) -> None:
|
||||
host.update_manifest(
|
||||
manifest_xml(
|
||||
f'<project name="platform/foo" revision="{rev}" '
|
||||
'upstream="main" />',
|
||||
# The skip only happens when fetching the current branch only.
|
||||
default='remote="origin" revision="main" sync-c="true"',
|
||||
)
|
||||
)
|
||||
|
||||
initial = foo.rev_parse("main")
|
||||
pin(initial)
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
|
||||
path = "platform/foo"
|
||||
second = foo.commit({"f": "2"})
|
||||
latest = foo.commit({"f": "3"})
|
||||
# Emulate a background prefetch that doesn't touch the tracking refs.
|
||||
client.git(
|
||||
path, "fetch", "-q", "--refmap=", "origin", "+main:refs/prefetch/main"
|
||||
)
|
||||
assert client.has_object(path, second)
|
||||
assert client.git(path, "rev-parse", "refs/remotes/origin/main") == initial
|
||||
pin(second)
|
||||
client.sync()
|
||||
|
||||
assert client.head(path) == second
|
||||
assert client.git(path, "rev-parse", "refs/remotes/origin/main") == latest
|
||||
|
||||
|
||||
def test_nested_projects(host: GitHost, client: RepoClient) -> None:
|
||||
"""Nested project paths are checked out and removed in the right order.
|
||||
|
||||
Regression test for 4679022 (b/325119758).
|
||||
"""
|
||||
foo = host.create_repo("platform/foo", {"f": "foo", "bar/f": "old"})
|
||||
host.create_repo("platform/bar", {"f": "foo/bar"})
|
||||
host.create_repo("platform/foo-bar", {"f": "foo-bar"})
|
||||
# aaa shares foo/bar's project, so foo/bar is grouped with it & checked out
|
||||
# early unless the checkout order puts it after its parent foo.
|
||||
host.create_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/bar" path="aaa" />',
|
||||
'<project name="platform/foo" path="foo" />',
|
||||
'<project name="platform/foo-bar" path="foo-bar" />',
|
||||
)
|
||||
)
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
|
||||
# Move foo's bar/f into the new nested project foo/bar.
|
||||
foo.commit({"bar/f": None})
|
||||
host.update_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/bar" path="aaa" />',
|
||||
'<project name="platform/foo" path="foo" />',
|
||||
'<project name="platform/bar" path="foo/bar" />',
|
||||
'<project name="platform/foo-bar" path="foo-bar" />',
|
||||
)
|
||||
)
|
||||
client.sync("-j1")
|
||||
|
||||
for path in ("foo", "foo/bar", "foo-bar"):
|
||||
assert (client.top / path / "f").read_text() == path
|
||||
assert client.git("foo/bar", "status", "--porcelain") == ""
|
||||
|
||||
host.update_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/foo" path="foo" />',
|
||||
'<project name="platform/foo-bar" path="foo-bar" />',
|
||||
)
|
||||
)
|
||||
client.sync()
|
||||
|
||||
assert set(client.list_projects()) == {"foo", "foo-bar"}
|
||||
assert not (client.top / "foo/bar").exists()
|
||||
assert (client.top / "foo/f").read_text() == "foo"
|
||||
|
||||
|
||||
def test_copyfile_linkfile(host: GitHost, client: RepoClient) -> None:
|
||||
"""copyfile and linkfile are created, updated and cleaned up.
|
||||
|
||||
linkfile symlinks are relative (0184dcc) so trees can be moved.
|
||||
"""
|
||||
foo = host.create_repo(
|
||||
"platform/foo", {"copy.txt": "v1", "dir/link.txt": "link"}
|
||||
)
|
||||
host.create_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/foo" path="foo">'
|
||||
'<copyfile src="copy.txt" dest="copied.txt" />'
|
||||
'<linkfile src="dir/link.txt" dest="links/link.txt" />'
|
||||
"</project>"
|
||||
)
|
||||
)
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
|
||||
link = client.top / "links/link.txt"
|
||||
# Python <3.8 on Windows rejects Path objects here.
|
||||
target = os.readlink(str(link))
|
||||
assert target == os.path.join("..", "foo", "dir", "link.txt")
|
||||
assert link.read_text() == "link"
|
||||
assert (client.top / "copied.txt").read_text() == "v1"
|
||||
|
||||
foo.commit({"copy.txt": "v2"})
|
||||
host.update_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/foo" path="foo">'
|
||||
'<copyfile src="copy.txt" dest="copied.txt" />'
|
||||
"</project>"
|
||||
)
|
||||
)
|
||||
client.sync()
|
||||
|
||||
assert (client.top / "copied.txt").read_text() == "v2"
|
||||
assert not os.path.lexists(link)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sync_args", [[], ["--no-interleaved"]])
|
||||
def test_submodules(
|
||||
host: GitHost, client: RepoClient, sync_args: List[str]
|
||||
) -> None:
|
||||
"""sync-s checks out & updates submodules, incl. for repeated projects.
|
||||
|
||||
Regression tests for 4623264 (b/444366154, `git submodule init` in
|
||||
interleaved sync), 1b4e7a0 (the same repo checked out at multiple paths),
|
||||
and 3a6e25a/3f087a8 (b/550074864, submodules follow their parent's new
|
||||
revision in the same interleaved & phased sync).
|
||||
"""
|
||||
sub = host.create_repo("platform/sub", {"s": "1"})
|
||||
gitmodules = '[submodule "sub"]\n\tpath = sub\n\turl = ../sub\n'
|
||||
foo = host.create_repo(
|
||||
"platform/foo",
|
||||
{".gitmodules": gitmodules, "sub": Gitlink(sub.rev_parse("main"))},
|
||||
)
|
||||
paths = ("a", "b")
|
||||
# Track a branch, as gitlinks are only stale (b/550074864) when the
|
||||
# parent's revision resolves before it's fetched (unlike new pinned SHAs).
|
||||
# With sync-c, phased syncs skip fetching the stale (present) revision.
|
||||
host.create_manifest(
|
||||
manifest_xml(
|
||||
*(
|
||||
f'<project name="platform/foo" path="{p}" sync-s="true" '
|
||||
'sync-c="true" />'
|
||||
for p in paths
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
client.init(host.manifest_url)
|
||||
client.sync(*sync_args)
|
||||
for path in paths:
|
||||
assert (client.top / path / "sub/s").read_text() == "1"
|
||||
assert client.git(path, "config", "--get", "submodule.sub.url")
|
||||
|
||||
latest = foo.commit({"sub": Gitlink(sub.commit({"s": "2"}))})
|
||||
client.sync(*sync_args)
|
||||
|
||||
for path in paths:
|
||||
assert client.head(path) == latest
|
||||
assert (client.top / path / "sub/s").read_text() == "2"
|
||||
|
||||
|
||||
# The default layout reads HEAD from the ref cache instead of GetHead().
|
||||
@pytest.mark.parametrize("init_args", [[], ["--worktree"]])
|
||||
def test_prune_detached_head(
|
||||
host: GitHost, client: RepoClient, init_args: List[str]
|
||||
) -> None:
|
||||
"""`repo prune` handles projects with a detached HEAD.
|
||||
|
||||
Regression test for 7f7d70e (b/434077990).
|
||||
"""
|
||||
host.create_repo("platform/foo", {"f": "foo"})
|
||||
host.create_manifest(manifest_xml('<project name="platform/foo" />'))
|
||||
client.init(host.manifest_url, *init_args)
|
||||
client.sync()
|
||||
path = "platform/foo"
|
||||
client.run("start", "topic", "--all")
|
||||
client.git(path, "checkout", "-q", "--detach")
|
||||
|
||||
client.run("prune")
|
||||
|
||||
assert not client.git(path, "branch", "--list", "topic")
|
||||
|
||||
|
||||
def test_submanifest_groups(host: GitHost, client: RepoClient) -> None:
|
||||
"""Submanifests apply their own default groups.
|
||||
|
||||
Regression test for cc88be3 (b/382086294).
|
||||
"""
|
||||
for name in ("parent", "child", "child-extra"):
|
||||
host.create_repo(f"platform/{name}", {"f": name})
|
||||
host.create_repo(
|
||||
"platform/child-manifest",
|
||||
{
|
||||
"default.xml": manifest_xml(
|
||||
'<project name="platform/child" path="child" groups="g1" />',
|
||||
'<project name="platform/child-extra" path="extra" />',
|
||||
)
|
||||
},
|
||||
)
|
||||
host.create_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/parent" path="parent" />',
|
||||
'<submanifest name="sub" project="platform/child-manifest" '
|
||||
'revision="main" path="sub" default-groups="g1" />',
|
||||
)
|
||||
)
|
||||
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
|
||||
assert client.list_projects() == {
|
||||
"parent": "platform/parent",
|
||||
"sub/child": "platform/child",
|
||||
}
|
||||
assert (client.top / "sub/child/f").read_text() == "child"
|
||||
assert not (client.top / "sub/extra").exists()
|
||||
@@ -0,0 +1,261 @@
|
||||
# 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 multi-step workflows, many from past point releases."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
from harness import GIT_DEFAULTS_PARAMS
|
||||
from harness import GitHost
|
||||
from harness import manifest_xml
|
||||
from harness import RepoClient
|
||||
from harness import run_git
|
||||
from harness import ServerRepo
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("git_defaults", GIT_DEFAULTS_PARAMS)
|
||||
@pytest.mark.parametrize(
|
||||
"project_attrs",
|
||||
["", 'clone-depth="1"', 'sync-strategy="stateless"'],
|
||||
)
|
||||
def test_two_phase_sync(
|
||||
host: GitHost, client: RepoClient, project_attrs: str
|
||||
) -> None:
|
||||
"""`repo sync -n` then `repo sync -l` before worktrees exist.
|
||||
|
||||
Regression tests for b/565047698 (v2.68.1, incl. e1e215a for stateless
|
||||
syncs).
|
||||
"""
|
||||
foo = host.create_repo("platform/foo", {"f": "1"})
|
||||
host.create_manifest(
|
||||
manifest_xml(
|
||||
f'<project name="platform/foo" path="foo" {project_attrs} />'
|
||||
)
|
||||
)
|
||||
client.init(host.manifest_url)
|
||||
|
||||
# The b/565047698 regression only logs a warning & still exits 0, so also
|
||||
# require a clean stderr.
|
||||
assert client.sync("-n").stderr == ""
|
||||
assert not (client.top / "foo").exists()
|
||||
# A second network-only sync (after upstream moved) with no worktree yet.
|
||||
latest = foo.commit({"f": "2"})
|
||||
assert client.sync("-n").stderr == ""
|
||||
assert not (client.top / "foo").exists()
|
||||
|
||||
client.sync("-l")
|
||||
assert client.head("foo") == latest
|
||||
assert (client.top / "foo/f").read_text() == "2"
|
||||
|
||||
# And the checkout keeps working with regular syncs afterwards.
|
||||
latest = foo.commit({"f": "3"})
|
||||
client.sync()
|
||||
assert client.head("foo") == latest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"extra_init_args", [[], ["--manifest-upstream-branch", "main"]]
|
||||
)
|
||||
def test_init_manifest_commit(
|
||||
host: GitHost, client: RepoClient, extra_init_args: List[str]
|
||||
) -> None:
|
||||
"""`repo init -b <sha>` pins the manifest to a raw commit.
|
||||
|
||||
Regression test for b/544041102 (v2.66.1).
|
||||
"""
|
||||
host.create_repo("platform/foo", {"f": "foo"})
|
||||
host.create_repo("platform/bar", {"f": "bar"})
|
||||
manifest = host.create_manifest(
|
||||
manifest_xml('<project name="platform/foo" />')
|
||||
)
|
||||
pinned = manifest.rev_parse("main")
|
||||
host.update_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/foo" />',
|
||||
'<project name="platform/bar" />',
|
||||
)
|
||||
)
|
||||
|
||||
client.init(host.manifest_url, "-b", pinned, *extra_init_args)
|
||||
client.sync()
|
||||
|
||||
assert client.list_projects() == {"platform/foo": "platform/foo"}
|
||||
assert client.git(".repo/manifests", "rev-parse", "HEAD") == pinned
|
||||
|
||||
|
||||
@pytest.mark.parametrize("git_defaults", GIT_DEFAULTS_PARAMS)
|
||||
def test_mirror(host: GitHost, client: RepoClient) -> None:
|
||||
"""`repo init --mirror && repo sync` creates bare mirrors (v2.57.1)."""
|
||||
foo = host.create_repo("platform/foo", {"f": "1"})
|
||||
foo.commit({"f": "other"}, branch="other")
|
||||
host.create_manifest(manifest_xml('<project name="platform/foo" />'))
|
||||
|
||||
client.init(host.manifest_url, "--mirror")
|
||||
client.sync()
|
||||
|
||||
mirror = client.top / "platform/foo.git"
|
||||
|
||||
def mirror_rev(ref: str) -> str:
|
||||
return run_git(f"--git-dir={mirror}", "rev-parse", ref, env=client.env)
|
||||
|
||||
assert not (client.top / "platform/foo").exists()
|
||||
assert mirror_rev("refs/heads/main") == foo.rev_parse("main")
|
||||
assert mirror_rev("refs/heads/other") == foo.rev_parse("other")
|
||||
|
||||
latest = foo.commit({"f": "2"})
|
||||
client.sync()
|
||||
assert mirror_rev("refs/heads/main") == latest
|
||||
|
||||
|
||||
def test_incremental_sync(host: GitHost, client: RepoClient) -> None:
|
||||
"""Syncs pick up new commits, new projects & removed projects."""
|
||||
foo = host.create_repo("platform/foo", {"f": "1"})
|
||||
host.create_repo("platform/bar", {"f": "bar"})
|
||||
host.create_repo("platform/baz", {"f": "baz"})
|
||||
host.create_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/foo" path="foo" />',
|
||||
'<project name="platform/bar" path="bar" />',
|
||||
)
|
||||
)
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
|
||||
latest = foo.commit({"f": "2", "new": "new"})
|
||||
host.update_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/foo" path="foo" />',
|
||||
'<project name="platform/baz" path="baz" />',
|
||||
)
|
||||
)
|
||||
client.sync()
|
||||
|
||||
assert client.head("foo") == latest
|
||||
assert (client.top / "foo/new").read_text() == "new"
|
||||
assert (client.top / "baz/f").read_text() == "baz"
|
||||
assert not (client.top / "bar").exists()
|
||||
assert client.list_projects() == {
|
||||
"baz": "platform/baz",
|
||||
"foo": "platform/foo",
|
||||
}
|
||||
|
||||
|
||||
def test_rebase_local_work(host: GitHost, client: RepoClient) -> None:
|
||||
"""Local commits on a `repo start` branch are rebased by sync."""
|
||||
foo = host.create_repo("platform/foo", {"f": "1"})
|
||||
host.create_manifest(manifest_xml('<project name="platform/foo" />'))
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
|
||||
path = "platform/foo"
|
||||
client.run("start", "topic", "--all")
|
||||
(client.top / path / "local").write_text("local")
|
||||
client.git(path, "add", "local")
|
||||
client.git(path, "commit", "-q", "-m", "local change")
|
||||
latest = foo.commit({"f": "2"})
|
||||
|
||||
client.sync()
|
||||
|
||||
assert client.git(path, "symbolic-ref", "--short", "HEAD") == "topic"
|
||||
assert client.git(path, "rev-parse", "HEAD~1") == latest
|
||||
assert (client.top / path / "local").read_text() == "local"
|
||||
|
||||
|
||||
def test_switch_manifest_branch(host: GitHost, client: RepoClient) -> None:
|
||||
"""Re-running `repo init -b` on an existing checkout switches branches."""
|
||||
foo = host.create_repo("platform/foo", {"f": "main"})
|
||||
release = foo.commit({"f": "release"}, branch="release")
|
||||
host.create_manifest(
|
||||
manifest_xml('<project name="platform/foo" path="foo" />')
|
||||
)
|
||||
host.update_manifest(
|
||||
manifest_xml(
|
||||
'<project name="platform/foo" path="foo" />',
|
||||
default='remote="origin" revision="release"',
|
||||
),
|
||||
branch="release",
|
||||
)
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
assert client.head("foo") == foo.rev_parse("main")
|
||||
|
||||
client.init(host.manifest_url, "-b", "release")
|
||||
client.sync()
|
||||
|
||||
assert client.head("foo") == release
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command",
|
||||
[
|
||||
["selfupdate", "--no-repo-verify"],
|
||||
["sync", "--no-repo-verify"],
|
||||
],
|
||||
)
|
||||
def test_selfupdate(
|
||||
host: GitHost,
|
||||
client: RepoClient,
|
||||
repo_source: ServerRepo,
|
||||
command: List[str],
|
||||
) -> None:
|
||||
"""repo upgrades itself when its source moves forward."""
|
||||
source = host.fork(repo_source, "git-repo")
|
||||
client.repo_url = source.url
|
||||
host.create_repo("platform/foo", {"f": "foo"})
|
||||
host.create_manifest(manifest_xml('<project name="platform/foo" />'))
|
||||
# Start from an existing synced checkout before publishing the repo update.
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
|
||||
latest = source.commit({"E2E_SELFUPDATE_MARKER": "new"})
|
||||
# sync only checks for updates once a day.
|
||||
fetch_head = client.repodir / "repo/.git/FETCH_HEAD"
|
||||
old = time.time() - 2 * 24 * 60 * 60
|
||||
os.utime(fetch_head, (old, old))
|
||||
|
||||
result = client.run(*command)
|
||||
|
||||
assert "A new version of repo is available" in result.stderr
|
||||
assert client.git(".repo/repo", "rev-parse", "HEAD") == latest
|
||||
assert (client.repodir / "repo/E2E_SELFUPDATE_MARKER").exists()
|
||||
# The restarted (upgraded) repo keeps working.
|
||||
assert "v99.0-e2e-1-g" in client.run("version").stdout
|
||||
|
||||
|
||||
def test_commands_after_sync(host: GitHost, client: RepoClient) -> None:
|
||||
"""Common read-only commands work on a freshly synced checkout."""
|
||||
host.create_repo("platform/foo", {"f": "foo"})
|
||||
host.create_manifest(manifest_xml('<project name="platform/foo" />'))
|
||||
client.init(host.manifest_url)
|
||||
client.sync()
|
||||
|
||||
for command, expected in (
|
||||
(["status"], r"nothing to commit"),
|
||||
(["info"], r"Manifest branch: main"),
|
||||
(["branches"], r"\(no branches\)"),
|
||||
# No local changes, so no output.
|
||||
(["diff"], r"\A\Z"),
|
||||
(["forall", "-c", "git", "log", "-1"], r"platform/foo: initial"),
|
||||
# Projects are pinned to commit ids.
|
||||
(["manifest", "-r"], r'revision="[0-9a-f]{40}"'),
|
||||
):
|
||||
output = client.run(*command).output
|
||||
assert re.search(expected, output), (
|
||||
f"`repo {' '.join(command)}` output {output!r} "
|
||||
f"did not match {expected!r}"
|
||||
)
|
||||
Reference in new issue
Block a user