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:
Rahul Yadav authored and gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com committed 2026-10-01 03:13:33 -07:00
1 parent 5d4149f32e
commit d53a4278f5
9 files changed
+527 -33

No files matched your search

+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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:
+16
View File
@@ -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
View File
@@ -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)
+56
View File
@@ -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
View File
@@ -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):