mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-10-15 09:58:04 +00:00
sync: Support manifest-driven default smart sync
Add support for enabling smart sync by default via the sync-smartsync="true" attribute on the manifest <default> element. When sync-smartsync is set to true in the manifest defaults: - Plain `repo sync` triggers Smart Sync automatically. - If that implicit Smart Sync fails (manifest server unreachable, or the returned manifest cannot be parsed), repo silently falls back to a regular ToT sync. The reason is logged with --verbose. - `--no-smart-sync` CLI flag overrides the manifest default and syncs to ToT. - Passing explicit `-s` / `--smart-sync` forces Smart Sync as before, and its failures are still fatal. - `-t`, `--smart-tag`, `-m`, `--manifest-name`, and `--superproject-revision` disable the default smart sync. - `-l` / `--local-only` and `--no-manifest-update` skip the manifest server and reuse the existing smart_sync_override.xml, if any. Update docs, completion, regenerate man pages, and add tests for XML parsing, serialization, CLI option resolution, and the fallback and reuse paths. Bug: 545137090 Change-Id: If75569c58dd525d170e9801e70ccbf02206a52f2 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/617641 Tested-by: Rahul Yadav <yadavrah@google.com> Commit-Queue: Rahul Yadav <yadavrah@google.com> Reviewed-by: Gavin Mak <gavinmak@google.com>
This commit is contained in:
1 parent
5d4149f32e
commit
d53a4278f5
9 files changed
+527
-33
No files matched your search
@@ -337,6 +337,7 @@ _repo() {
|
||||
'--auto-gc[Run auto gc]' \
|
||||
'--no-auto-gc[Do not run auto gc]' \
|
||||
'(-s --smart-sync)'{-s,--smart-sync}'[Smart sync]' \
|
||||
'--no-smart-sync[Do not smart sync]' \
|
||||
'(-t --smart-tag)'{-t,--smart-tag=}'[Smart tag]:tag:' \
|
||||
'--no-repo-verify[Do not verify repo]' \
|
||||
'--no-verify[Do not verify]' \
|
||||
|
||||
+13
-10
@@ -46,18 +46,19 @@ following DTD:
|
||||
<!ATTLIST remote revision CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT default EMPTY>
|
||||
<!ATTLIST default remote IDREF #IMPLIED>
|
||||
<!ATTLIST default revision CDATA #IMPLIED>
|
||||
<!ATTLIST default dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-j-max CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-tags CDATA #IMPLIED>
|
||||
<!ATTLIST default remote IDREF #IMPLIED>
|
||||
<!ATTLIST default revision CDATA #IMPLIED>
|
||||
<!ATTLIST default dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-j-max CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-tags CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-smartsync CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT manifest-server EMPTY>
|
||||
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest-server helper CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
@@ -231,6 +232,8 @@ Attribute `sync-tags`: Set to false to only sync the given Git
|
||||
branch (specified in the `revision` attribute) rather than
|
||||
the other ref tags.
|
||||
|
||||
Attribute `sync-smartsync`: Set to true to enable smart sync by default.
|
||||
|
||||
|
||||
### Element manifest-server
|
||||
|
||||
|
||||
+35
-3
@@ -138,9 +138,36 @@ copy into `.repo/manifests/smart_sync_override.xml` so users can examine it.
|
||||
The next time `repo sync` is run, this file is automatically replaced or removed
|
||||
based on the current set of options.
|
||||
|
||||
### --smart-sync
|
||||
### sync-smartsync (Manifest Attribute)
|
||||
|
||||
Repo will call `GetApprovedManifest(branch[, target])`.
|
||||
The manifest can enable Smart Sync by default for all `repo sync` invocations
|
||||
by setting `sync-smartsync="true"` on the `<default>` element:
|
||||
|
||||
```xml
|
||||
<default sync-smartsync="true" ... />
|
||||
```
|
||||
|
||||
When enabled in the manifest, `repo sync` will automatically perform a Smart
|
||||
Sync unless explicitly overridden on the command line. Since the user did not
|
||||
ask for a Smart Sync, any failure to obtain a manifest from the manifest server
|
||||
(e.g. the server is unreachable, or it returns a manifest that cannot be parsed)
|
||||
is silently ignored and `repo sync` falls back to a regular ToT sync; pass
|
||||
`--verbose` to see the reason. This is only a fallback for the manifest-driven
|
||||
default: an explicit `-s`/`--smart-sync` or `-t`/`--smart-tag` still fails the
|
||||
sync, as before. Options that specify an explicit target or manifest source
|
||||
(such as `-t`/`--smart-tag`, `-m`/`--manifest-name` or
|
||||
`--superproject-revision`) will also disable the default Smart Sync behavior.
|
||||
|
||||
With `-l`/`--local-only` or `--no-manifest-update`, the default Smart Sync does
|
||||
not contact the manifest server. Instead, repo keeps using the
|
||||
`smart_sync_override.xml` from the last Smart Sync, if there is one. Pass
|
||||
`--no-smart-sync` to discard it and use the default manifest instead. These
|
||||
options do not change the behavior of an explicit `-s`/`--smart-sync`.
|
||||
|
||||
### --smart-sync / -s
|
||||
|
||||
Explicitly enables Smart Sync. Repo will call
|
||||
`GetApprovedManifest(branch[, target])`.
|
||||
|
||||
The `branch` is determined by the current manifest branch as specified by
|
||||
`--manifest-branch=BRANCH` when running `repo init`.
|
||||
@@ -155,6 +182,11 @@ match the settings Android build environments automatically setup.
|
||||
3. `${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}`: If these variables are all
|
||||
defined, then they are merged with `-` and used.
|
||||
|
||||
### --smart-tag=TAG
|
||||
### --no-smart-sync
|
||||
|
||||
Explicitly disables Smart Sync, overriding any `sync-smartsync="true"` setting
|
||||
declared in the manifest `<default>` element.
|
||||
|
||||
### --smart-tag=TAG / -t
|
||||
|
||||
Repo will call `GetManifest(TAG)`.
|
||||
+14
-11
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "June 2026" "repo manifest" "Repo Manual"
|
||||
.TH REPO "1" "August 2026" "repo manifest" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo manifest - manual page for repo manifest
|
||||
.SH SYNOPSIS
|
||||
@@ -121,18 +121,19 @@ include*)>
|
||||
<!ATTLIST remote revision CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT default EMPTY>
|
||||
<!ATTLIST default remote IDREF #IMPLIED>
|
||||
<!ATTLIST default revision CDATA #IMPLIED>
|
||||
<!ATTLIST default dest\-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-j\-max CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-tags CDATA #IMPLIED>
|
||||
<!ATTLIST default remote IDREF #IMPLIED>
|
||||
<!ATTLIST default revision CDATA #IMPLIED>
|
||||
<!ATTLIST default dest\-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-j\-max CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-tags CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-smartsync CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT manifest\-server EMPTY>
|
||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest\-server helper CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
@@ -336,6 +337,8 @@ Attribute `sync\-s`: Set to true to also sync sub\-projects.
|
||||
Attribute `sync\-tags`: Set to false to only sync the given Git branch (specified
|
||||
in the `revision` attribute) rather than the other ref tags.
|
||||
.PP
|
||||
Attribute `sync\-smartsync`: Set to true to enable smart sync by default.
|
||||
.PP
|
||||
Element manifest\-server
|
||||
.PP
|
||||
At most one manifest\-server may be specified. The url attribute is used to
|
||||
|
||||
@@ -125,6 +125,9 @@ do not run garbage collection on any projects (default)
|
||||
\fB\-s\fR, \fB\-\-smart\-sync\fR
|
||||
smart sync using manifest from the latest known good build
|
||||
.TP
|
||||
\fB\-\-no\-smart\-sync\fR
|
||||
disable smart sync and sync to ToT instead
|
||||
.TP
|
||||
\fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR
|
||||
smart sync using manifest from a known tag
|
||||
.SS Logging options:
|
||||
|
||||
@@ -159,6 +159,7 @@ class _Default:
|
||||
sync_c = False
|
||||
sync_s = False
|
||||
sync_tags = True
|
||||
sync_smartsync = False
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, _Default):
|
||||
@@ -483,6 +484,17 @@ class XmlManifest:
|
||||
self.Unload()
|
||||
self._Load()
|
||||
|
||||
def ClearOverride(self) -> None:
|
||||
"""Stop overriding the manifest, reverting to the default one.
|
||||
|
||||
This is the inverse of Override(), and is a no-op if no override is in
|
||||
effect. It is useful to recover from a failed Override(), which
|
||||
registers the override before parsing it.
|
||||
"""
|
||||
self._outer_client.manifestFileOverrides.pop(self.path_prefix, None)
|
||||
self._load_local_manifests = True
|
||||
self.Unload()
|
||||
|
||||
def Link(self, name):
|
||||
"""Update the repo metadata to use a different manifest."""
|
||||
self.Override(name)
|
||||
@@ -644,6 +656,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if not d.sync_tags:
|
||||
have_default = True
|
||||
e.setAttribute("sync-tags", "false")
|
||||
if d.sync_smartsync:
|
||||
have_default = True
|
||||
e.setAttribute("sync-smartsync", "true")
|
||||
if have_default:
|
||||
root.appendChild(e)
|
||||
root.appendChild(doc.createTextNode(""))
|
||||
@@ -1789,6 +1804,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
d.sync_c = XmlBool(node, "sync-c", False)
|
||||
d.sync_s = XmlBool(node, "sync-s", False)
|
||||
d.sync_tags = XmlBool(node, "sync-tags", True)
|
||||
d.sync_smartsync = XmlBool(node, "sync-smartsync", False)
|
||||
return d
|
||||
|
||||
def _ParseNotice(self, node):
|
||||
|
||||
+87
-8
@@ -58,6 +58,7 @@ from command import DEFAULT_LOCAL_JOBS
|
||||
from command import MirrorSafeCommand
|
||||
from command import WORKER_BATCH_SIZE
|
||||
from error import GitError
|
||||
from error import ManifestParseError
|
||||
from error import RepoChangedException
|
||||
from error import RepoError
|
||||
from error import RepoExitError
|
||||
@@ -764,9 +765,16 @@ later is required to fix a server side protocol bug.
|
||||
"-s",
|
||||
"--smart-sync",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="smart sync using manifest from the latest known good "
|
||||
"build",
|
||||
)
|
||||
p.add_option(
|
||||
"--no-smart-sync",
|
||||
dest="smart_sync",
|
||||
action="store_false",
|
||||
help="disable smart sync and sync to ToT instead",
|
||||
)
|
||||
p.add_option(
|
||||
"-t",
|
||||
"--smart-tag",
|
||||
@@ -2249,10 +2257,6 @@ later is required to fix a server side protocol bug.
|
||||
if opt.manifest_name and opt.smart_tag:
|
||||
self.OptionParser.error("cannot combine -m and -t")
|
||||
if opt.manifest_server_username or opt.manifest_server_password:
|
||||
if not (opt.smart_sync or opt.smart_tag):
|
||||
self.OptionParser.error(
|
||||
"-u and -p may only be combined with -s or -t"
|
||||
)
|
||||
if None in [
|
||||
opt.manifest_server_username,
|
||||
opt.manifest_server_password,
|
||||
@@ -2413,6 +2417,34 @@ later is required to fix a server side protocol bug.
|
||||
"failed to sync manifest project", aggregate_errors=[e]
|
||||
)
|
||||
|
||||
def _ResolveSmartSyncOption(
|
||||
self, opt: optparse.Values, manifest: XmlManifest
|
||||
) -> bool:
|
||||
"""Resolve opt.smart_sync from the CLI flags and manifest default.
|
||||
|
||||
Returns:
|
||||
True if smart_sync was enabled implicitly by the manifest default,
|
||||
False otherwise.
|
||||
"""
|
||||
implicit = (
|
||||
opt.smart_sync is None
|
||||
and not opt.smart_tag
|
||||
and not opt.manifest_name
|
||||
and not opt.superproject_revision
|
||||
and not opt.local_only
|
||||
and opt.mp_update
|
||||
and getattr(manifest.default, "sync_smartsync", False) is True
|
||||
)
|
||||
if opt.smart_sync is None:
|
||||
opt.smart_sync = implicit
|
||||
|
||||
if (
|
||||
opt.manifest_server_username or opt.manifest_server_password
|
||||
) and not (opt.smart_sync or opt.smart_tag):
|
||||
raise SmartSyncError("-u and -p may only be combined with -s or -t")
|
||||
|
||||
return implicit
|
||||
|
||||
def _ExecuteHelper(self, opt, args, errors):
|
||||
manifest = self.outer_manifest
|
||||
if not opt.outer_manifest:
|
||||
@@ -2427,11 +2459,58 @@ later is required to fix a server side protocol bug.
|
||||
if opt.clone_bundle is None:
|
||||
opt.clone_bundle = manifest.CloneBundle
|
||||
|
||||
# An onboarded client run with -l or --no-manifest-update keeps the
|
||||
# smart sync manifest it last synced to instead of contacting the
|
||||
# manifest server. Computed before _ResolveSmartSyncOption, which
|
||||
# makes an unset opt.smart_sync indistinguishable from --no-smart-sync.
|
||||
reuse_smart_sync_override = (
|
||||
(opt.local_only or not opt.mp_update)
|
||||
and opt.smart_sync is None
|
||||
and not opt.smart_tag
|
||||
and not opt.manifest_name
|
||||
and not opt.superproject_revision
|
||||
and getattr(manifest.default, "sync_smartsync", False) is True
|
||||
and os.path.isfile(smart_sync_manifest_path)
|
||||
)
|
||||
|
||||
implicit_smart_sync = self._ResolveSmartSyncOption(opt, manifest)
|
||||
|
||||
if opt.smart_sync or opt.smart_tag:
|
||||
manifest_name = self._SmartSyncSetup(
|
||||
opt, smart_sync_manifest_path, manifest
|
||||
)
|
||||
else:
|
||||
try:
|
||||
manifest_name = self._SmartSyncSetup(
|
||||
opt, smart_sync_manifest_path, manifest
|
||||
)
|
||||
except (SmartSyncError, ManifestParseError) as e:
|
||||
if not implicit_smart_sync:
|
||||
raise
|
||||
# The user did not ask for a smart sync, so failing to get a
|
||||
# usable manifest from the server should not fail the sync.
|
||||
# Fall back to a regular ToT sync instead.
|
||||
opt.smart_sync = False
|
||||
# Override() records the override before parsing it, so an
|
||||
# unparsable manifest would otherwise leave the client pinned
|
||||
# to a file it cannot load.
|
||||
manifest.ClearOverride()
|
||||
if opt.verbose:
|
||||
logger.warning(
|
||||
"warning: smart sync failed; falling back to ToT: %s", e
|
||||
)
|
||||
elif reuse_smart_sync_override:
|
||||
manifest_name = os.path.basename(smart_sync_manifest_path)
|
||||
try:
|
||||
self._ReloadManifest(manifest_name, manifest)
|
||||
except ManifestParseError as e:
|
||||
reuse_smart_sync_override = False
|
||||
manifest_name = None
|
||||
manifest.ClearOverride()
|
||||
if opt.verbose:
|
||||
logger.warning(
|
||||
"warning: failed to load existing smart sync manifest; "
|
||||
"falling back to ToT: %s",
|
||||
e,
|
||||
)
|
||||
|
||||
if not (opt.smart_sync or opt.smart_tag or reuse_smart_sync_override):
|
||||
if os.path.isfile(smart_sync_manifest_path):
|
||||
try:
|
||||
platform_utils.remove(smart_sync_manifest_path)
|
||||
|
||||
@@ -420,6 +420,62 @@ class TestXmlManifest:
|
||||
)
|
||||
manifest.ToXml()
|
||||
|
||||
def test_sync_smartsync(self, repo_client: RepoClient) -> None:
|
||||
"""Check sync-smartsync handling."""
|
||||
manifest = repo_client.get_xml_manifest(
|
||||
"<manifest><default /></manifest>"
|
||||
)
|
||||
assert manifest.default.sync_smartsync is False
|
||||
|
||||
manifest = repo_client.get_xml_manifest(
|
||||
'<manifest><default sync-smartsync="true" /></manifest>'
|
||||
)
|
||||
assert manifest.default.sync_smartsync is True
|
||||
assert (
|
||||
manifest.ToXml().toxml() == '<?xml version="1.0" ?>'
|
||||
'<manifest><default sync-smartsync="true"/></manifest>'
|
||||
)
|
||||
|
||||
manifest = repo_client.get_xml_manifest(
|
||||
'<manifest><default sync-smartsync="false" /></manifest>'
|
||||
)
|
||||
assert manifest.default.sync_smartsync is False
|
||||
assert manifest.ToXml().toxml() == '<?xml version="1.0" ?><manifest/>'
|
||||
|
||||
def test_clear_override(self, repo_client: RepoClient) -> None:
|
||||
"""Check ClearOverride reverts to the default manifest."""
|
||||
manifest_xml_fmt = (
|
||||
'<manifest><remote name="r" fetch="." />'
|
||||
'<default remote="r" revision="main" />'
|
||||
'<project name="%s" /></manifest>'
|
||||
)
|
||||
manifest = repo_client.get_xml_manifest(manifest_xml_fmt % "default")
|
||||
(repo_client.manifest_dir / "good.xml").write_text(
|
||||
manifest_xml_fmt % "override", encoding="utf-8"
|
||||
)
|
||||
(repo_client.manifest_dir / "bad.xml").write_text(
|
||||
"<manifest>", encoding="utf-8"
|
||||
)
|
||||
|
||||
# No override in effect: a no-op.
|
||||
manifest.ClearOverride()
|
||||
assert list(manifest.paths) == ["default"]
|
||||
|
||||
manifest.Override("good.xml")
|
||||
assert list(manifest.paths) == ["override"]
|
||||
manifest.ClearOverride()
|
||||
assert not manifest.manifestFileOverrides
|
||||
assert list(manifest.paths) == ["default"]
|
||||
|
||||
# Override() registers the override before parsing it, so a failed
|
||||
# Override() leaves the client pinned until ClearOverride().
|
||||
with pytest.raises(error.ManifestParseError):
|
||||
manifest.Override("bad.xml")
|
||||
with pytest.raises(error.ManifestParseError):
|
||||
manifest.Load()
|
||||
manifest.ClearOverride()
|
||||
assert list(manifest.paths) == ["default"]
|
||||
|
||||
|
||||
class TestIncludeElement:
|
||||
"""Tests for <include>."""
|
||||
|
||||
+302
-1
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Dict, List, Optional, Tuple, Type
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
@@ -29,6 +29,7 @@ import pytest
|
||||
|
||||
import command
|
||||
from error import GitError
|
||||
from error import ManifestParseError
|
||||
from error import RepoExitError
|
||||
import manifest_xml
|
||||
from project import SyncNetworkHalfResult
|
||||
@@ -390,6 +391,90 @@ def test_cli_jobs_sync_j_max(
|
||||
assert opts.jobs_checkout == jobs_check
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"argv, sync_smartsync_manifest, expected_smart_sync, expected_implicit",
|
||||
[
|
||||
([], False, False, False),
|
||||
([], None, False, False),
|
||||
([], True, True, True),
|
||||
(["-s"], False, True, False),
|
||||
(["--smart-sync"], False, True, False),
|
||||
(["--smart-sync"], True, True, False),
|
||||
(["--no-smart-sync"], True, False, False),
|
||||
(["--no-smart-sync"], False, False, False),
|
||||
(["-t", "tag123"], True, False, False),
|
||||
(["--smart-tag=tag123"], True, False, False),
|
||||
(["-m", "other.xml"], True, False, False),
|
||||
(["--manifest-name=other.xml"], True, False, False),
|
||||
(["-l"], True, False, False),
|
||||
(["--local-only"], True, False, False),
|
||||
(["--nmu"], True, False, False),
|
||||
(["--no-manifest-update"], True, False, False),
|
||||
(["--superproject-revision=abc"], True, False, False),
|
||||
(["-s", "-l"], True, True, False),
|
||||
(["-s", "--nmu"], True, True, False),
|
||||
(["-s", "--superproject-revision=abc"], True, True, False),
|
||||
],
|
||||
)
|
||||
def test_cli_smart_sync(
|
||||
argv: List[str],
|
||||
sync_smartsync_manifest: Optional[bool],
|
||||
expected_smart_sync: bool,
|
||||
expected_implicit: bool,
|
||||
) -> None:
|
||||
"""Tests --smart-sync and --no-smart-sync option behavior with manifest
|
||||
default.
|
||||
"""
|
||||
manifest = mock.MagicMock()
|
||||
manifest.default.sync_smartsync = sync_smartsync_manifest
|
||||
|
||||
cmd = sync.Sync(manifest=manifest)
|
||||
opts, args = cmd.OptionParser.parse_args(argv)
|
||||
cmd.ValidateOptions(opts, args)
|
||||
implicit = cmd._ResolveSmartSyncOption(opts, manifest)
|
||||
assert opts.smart_sync == expected_smart_sync
|
||||
assert implicit == expected_implicit
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"argv, sync_smartsync_manifest, expected_exception",
|
||||
[
|
||||
(["-u", "user", "-p", "pass"], False, sync.SmartSyncError),
|
||||
(["-u", "user", "-p", "pass"], True, None),
|
||||
(["-s", "-u", "user", "-p", "pass"], False, None),
|
||||
(["-t", "tag", "-u", "user", "-p", "pass"], False, None),
|
||||
(
|
||||
["--no-smart-sync", "-u", "user", "-p", "pass"],
|
||||
True,
|
||||
sync.SmartSyncError,
|
||||
),
|
||||
(["-l", "-u", "user", "-p", "pass"], True, sync.SmartSyncError),
|
||||
(["--nmu", "-u", "user", "-p", "pass"], True, sync.SmartSyncError),
|
||||
(["-s", "-l", "-u", "user", "-p", "pass"], True, None),
|
||||
(["-u", "user"], False, SystemExit),
|
||||
(["-p", "pass"], False, SystemExit),
|
||||
],
|
||||
)
|
||||
def test_cli_manifest_server_credentials(
|
||||
argv: List[str],
|
||||
sync_smartsync_manifest: bool,
|
||||
expected_exception: Optional[Type[BaseException]],
|
||||
) -> None:
|
||||
"""Tests -u and -p validation rules."""
|
||||
manifest = mock.MagicMock()
|
||||
manifest.default.sync_smartsync = sync_smartsync_manifest
|
||||
|
||||
cmd = sync.Sync(manifest=manifest)
|
||||
opts, args = cmd.OptionParser.parse_args(argv)
|
||||
if expected_exception:
|
||||
with pytest.raises(expected_exception):
|
||||
cmd.ValidateOptions(opts, args)
|
||||
cmd._ResolveSmartSyncOption(opts, manifest)
|
||||
else:
|
||||
cmd.ValidateOptions(opts, args)
|
||||
cmd._ResolveSmartSyncOption(opts, manifest)
|
||||
|
||||
|
||||
class LocalSyncState(unittest.TestCase):
|
||||
"""Tests for LocalSyncState."""
|
||||
|
||||
@@ -1484,6 +1569,193 @@ class SyncCommand(unittest.TestCase):
|
||||
phased, interleaved = self._ExecuteUntilSync([])
|
||||
self.assertTrue(phased.called or interleaved.called)
|
||||
|
||||
def test_implicit_smart_sync_fallback(self) -> None:
|
||||
"""Ensure implicit smart sync silently falls back to ToT."""
|
||||
self.manifest.default.sync_smartsync = True
|
||||
self.opt.smart_sync = None
|
||||
mock.patch.object(self.cmd, "_UpdateAllManifestProjects").start()
|
||||
self.opt.verbose = False
|
||||
with mock.patch.object(
|
||||
self.cmd,
|
||||
"_SmartSyncSetup",
|
||||
side_effect=sync.SmartSyncError("unreachable"),
|
||||
) as mock_setup:
|
||||
with mock.patch.object(self.cmd, "_UpdateRepoProject"):
|
||||
with mock.patch.object(
|
||||
self.cmd, "_ValidateOptionsWithManifest"
|
||||
):
|
||||
with mock.patch.object(self.cmd, "_SyncInterleaved"):
|
||||
with mock.patch.object(self.cmd, "_RunPostSyncHook"):
|
||||
with mock.patch.object(
|
||||
sync.logger, "warning"
|
||||
) as mock_warn:
|
||||
self.cmd.Execute(self.opt, [])
|
||||
mock_setup.assert_called_once()
|
||||
self.assertFalse(self.opt.smart_sync)
|
||||
self.assertFalse(mock_warn.called)
|
||||
|
||||
def test_implicit_smart_sync_fallback_verbose(self) -> None:
|
||||
"""Ensure the fallback reason is logged when --verbose is given."""
|
||||
self.manifest.default.sync_smartsync = True
|
||||
self.opt.smart_sync = None
|
||||
mock.patch.object(self.cmd, "_UpdateAllManifestProjects").start()
|
||||
self.opt.verbose = True
|
||||
with mock.patch.object(
|
||||
self.cmd,
|
||||
"_SmartSyncSetup",
|
||||
side_effect=sync.SmartSyncError("unreachable"),
|
||||
):
|
||||
with mock.patch.object(self.cmd, "_UpdateRepoProject"):
|
||||
with mock.patch.object(
|
||||
self.cmd, "_ValidateOptionsWithManifest"
|
||||
):
|
||||
with mock.patch.object(self.cmd, "_SyncInterleaved"):
|
||||
with mock.patch.object(self.cmd, "_RunPostSyncHook"):
|
||||
with mock.patch.object(
|
||||
sync.logger, "warning"
|
||||
) as mock_warn:
|
||||
self.cmd.Execute(self.opt, [])
|
||||
self.assertFalse(self.opt.smart_sync)
|
||||
self.assertTrue(mock_warn.called)
|
||||
|
||||
def test_explicit_smart_sync_error(self) -> None:
|
||||
"""Ensure explicit smart sync raises SmartSyncError on failure."""
|
||||
self.manifest.default.sync_smartsync = True
|
||||
self.opt.smart_sync = True
|
||||
self.opt.mp_update = False
|
||||
with mock.patch.object(
|
||||
self.cmd,
|
||||
"_SmartSyncSetup",
|
||||
side_effect=sync.SmartSyncError("unreachable"),
|
||||
):
|
||||
with self.assertRaises(sync.SmartSyncError):
|
||||
self.cmd.Execute(self.opt, [])
|
||||
|
||||
def test_implicit_smart_sync_unparsable_manifest(self) -> None:
|
||||
"""Ensure an unparsable smart sync manifest falls back to ToT."""
|
||||
self.manifest.default.sync_smartsync = True
|
||||
self.opt.smart_sync = None
|
||||
mock.patch.object(self.cmd, "_UpdateAllManifestProjects").start()
|
||||
self.opt.verbose = False
|
||||
with mock.patch.object(
|
||||
self.cmd,
|
||||
"_SmartSyncSetup",
|
||||
side_effect=ManifestParseError("mismatched tag"),
|
||||
):
|
||||
with mock.patch.object(self.cmd, "_UpdateRepoProject"):
|
||||
with mock.patch.object(
|
||||
self.cmd, "_ValidateOptionsWithManifest"
|
||||
):
|
||||
with mock.patch.object(self.cmd, "_SyncInterleaved"):
|
||||
with mock.patch.object(self.cmd, "_RunPostSyncHook"):
|
||||
self.cmd.Execute(self.opt, [])
|
||||
self.assertFalse(self.opt.smart_sync)
|
||||
# A half-applied override would pin the client to a broken manifest.
|
||||
self.manifest.ClearOverride.assert_called_once_with()
|
||||
|
||||
def test_explicit_smart_sync_unparsable_manifest(self) -> None:
|
||||
"""Ensure explicit smart sync still reports the parse error."""
|
||||
self.manifest.default.sync_smartsync = True
|
||||
self.opt.smart_sync = True
|
||||
self.opt.mp_update = False
|
||||
with mock.patch.object(
|
||||
self.cmd,
|
||||
"_SmartSyncSetup",
|
||||
side_effect=ManifestParseError("mismatched tag"),
|
||||
):
|
||||
with self.assertRaises(ManifestParseError):
|
||||
self.cmd.Execute(self.opt, [])
|
||||
|
||||
def _ExecuteWithSmartSyncOverride(
|
||||
self, reload_error: Optional[Exception] = None
|
||||
) -> Tuple[str, mock.MagicMock, mock.MagicMock, mock.MagicMock]:
|
||||
"""Run Execute with an existing smart sync override on disk.
|
||||
|
||||
Returns:
|
||||
The override path, and the _SmartSyncSetup, _ReloadManifest and
|
||||
_UpdateAllManifestProjects mocks.
|
||||
"""
|
||||
worktree = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, worktree)
|
||||
self.manifest.manifestProject.worktree = worktree
|
||||
# -l reloads the superproject manifest; keep that out of the way.
|
||||
self.manifest.superproject = None
|
||||
self.opt.verbose = False
|
||||
override_path = os.path.join(worktree, "smart_sync_override.xml")
|
||||
with open(override_path, "w") as f:
|
||||
f.write("<manifest />")
|
||||
|
||||
with contextlib.ExitStack() as stack:
|
||||
for name in (
|
||||
"_UpdateRepoProject",
|
||||
"_ValidateOptionsWithManifest",
|
||||
"_SyncInterleaved",
|
||||
"_RunPostSyncHook",
|
||||
):
|
||||
stack.enter_context(mock.patch.object(self.cmd, name))
|
||||
setup = stack.enter_context(
|
||||
mock.patch.object(self.cmd, "_SmartSyncSetup")
|
||||
)
|
||||
reload = stack.enter_context(
|
||||
mock.patch.object(
|
||||
self.cmd, "_ReloadManifest", side_effect=reload_error
|
||||
)
|
||||
)
|
||||
update_mps = stack.enter_context(
|
||||
mock.patch.object(self.cmd, "_UpdateAllManifestProjects")
|
||||
)
|
||||
self.cmd.Execute(self.opt, [])
|
||||
return override_path, setup, reload, update_mps
|
||||
|
||||
def test_smart_sync_override_reused_offline(self) -> None:
|
||||
"""Ensure -l/--nmu reuse an onboarded client's smart sync manifest."""
|
||||
for local_only, mp_update in ((True, True), (False, False)):
|
||||
with self.subTest(local_only=local_only, mp_update=mp_update):
|
||||
self.manifest.default.sync_smartsync = True
|
||||
self.opt.smart_sync = None
|
||||
self.opt.local_only = local_only
|
||||
self.opt.mp_update = mp_update
|
||||
path, setup, reload, update_mps = (
|
||||
self._ExecuteWithSmartSyncOverride()
|
||||
)
|
||||
setup.assert_not_called()
|
||||
reload.assert_called_once_with(
|
||||
"smart_sync_override.xml", self.manifest
|
||||
)
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
if mp_update:
|
||||
self.assertEqual(
|
||||
update_mps.call_args[0][2], "smart_sync_override.xml"
|
||||
)
|
||||
|
||||
def test_smart_sync_override_removed_offline(self) -> None:
|
||||
"""Ensure -l still drops the override when reuse does not apply."""
|
||||
cases = (
|
||||
("--no-smart-sync", False, True),
|
||||
("manifest without sync-smartsync", None, False),
|
||||
)
|
||||
for name, smart_sync, sync_smartsync in cases:
|
||||
with self.subTest(name):
|
||||
self.manifest.default.sync_smartsync = sync_smartsync
|
||||
self.opt.smart_sync = smart_sync
|
||||
self.opt.local_only = True
|
||||
path, setup, reload, _ = self._ExecuteWithSmartSyncOverride()
|
||||
setup.assert_not_called()
|
||||
reload.assert_not_called()
|
||||
self.assertFalse(os.path.isfile(path))
|
||||
|
||||
def test_smart_sync_override_unparsable_offline(self) -> None:
|
||||
"""Ensure an unloadable reused override falls back to ToT."""
|
||||
self.manifest.default.sync_smartsync = True
|
||||
self.opt.smart_sync = None
|
||||
self.opt.local_only = True
|
||||
path, _, _, update_mps = self._ExecuteWithSmartSyncOverride(
|
||||
reload_error=ManifestParseError("mismatched tag")
|
||||
)
|
||||
self.manifest.ClearOverride.assert_called_once_with()
|
||||
self.assertFalse(os.path.isfile(path))
|
||||
self.assertIsNone(update_mps.call_args[0][2])
|
||||
|
||||
|
||||
class SyncUpdateRepoProject(unittest.TestCase):
|
||||
"""Tests for Sync._UpdateRepoProject."""
|
||||
@@ -2501,6 +2773,35 @@ class TestSmartSyncSetupRemoteHelper(unittest.TestCase):
|
||||
|
||||
self.assertEqual(manifest_name, "manifest.xml")
|
||||
|
||||
@mock.patch("xmlrpc.client.Server")
|
||||
def test_smart_sync_setup_unparsable_manifest(
|
||||
self, mock_server_class: mock.MagicMock
|
||||
) -> None:
|
||||
"""Test _SmartSyncSetup when the server returns a bad manifest.
|
||||
|
||||
The parse error is left alone so that an explicit smart sync keeps
|
||||
reporting it as-is; recovering from it is up to the caller.
|
||||
"""
|
||||
self.manifest.manifest_server = (
|
||||
"http://android-smartsync.corp.google.com/manifestserver"
|
||||
)
|
||||
self.manifest.manifest_server_helper = None
|
||||
|
||||
mock_server = mock.MagicMock()
|
||||
mock_server.GetApprovedManifest.return_value = [True, "<manifest>"]
|
||||
mock_server_class.return_value = mock_server
|
||||
|
||||
self.cmd._GetBranch = mock.MagicMock(return_value="main")
|
||||
self.cmd._ReloadManifest = mock.MagicMock(
|
||||
side_effect=ManifestParseError("mismatched tag")
|
||||
)
|
||||
|
||||
with mock.patch("builtins.open", mock.mock_open()):
|
||||
with self.assertRaises(ManifestParseError):
|
||||
self.cmd._SmartSyncSetup(
|
||||
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||
)
|
||||
|
||||
@mock.patch("shutil.which")
|
||||
@mock.patch("subprocess.Popen")
|
||||
def test_smart_sync_setup_helper_error(self, mock_popen, mock_which):
|
||||
|
||||
Reference in new issue
Block a user