mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-07-16 05:36:59 +00:00
sync: Support pluggable remote helpers for smart sync manifest server.
Introduce support for pluggable remote helpers (declared via the optional 'helper' attribute in <manifest-server>) to dynamically resolve proxy addresses. Route the XML-RPC manifest server connection through the resolved proxy. Bug: b/517477903 Change-Id: I3b6b8ea2640bb077521df4b4a9e8a34a8c6ecdad Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/591642 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:
committed by
gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
cd307a6089
commit
39c0b60900
@@ -58,6 +58,7 @@ following DTD:
|
||||
|
||||
<!ELEMENT manifest-server EMPTY>
|
||||
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest-server helper CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
@@ -239,6 +240,11 @@ XML RPC service.
|
||||
|
||||
See the [smart sync documentation](./smart-sync.md) for more details.
|
||||
|
||||
Attribute `url`: The URL of the manifest server.
|
||||
|
||||
Attribute `helper`: Optional name of a remote helper binary to execute to
|
||||
resolve proxying or authentication for the manifest server.
|
||||
|
||||
|
||||
### Element submanifest
|
||||
|
||||
|
||||
+32
-1
@@ -27,13 +27,44 @@ the [`<manifest-server>` element](manifest-format.md#Element-manifest_server)
|
||||
element. This is how the client knows what service to talk to.
|
||||
|
||||
```xml
|
||||
<manifest-server url="https://example.com/your/manifest/server/url" />
|
||||
<manifest-server url="https://example.com/your/manifest/server/url"
|
||||
helper="repo-remote-helper-name" />
|
||||
```
|
||||
|
||||
If the URL starts with `persistent-`, then the
|
||||
[`git-remote-persistent-https` helper](https://github.com/git/git/blob/HEAD/contrib/persistent-https/README)
|
||||
is used to communicate with the server.
|
||||
|
||||
### Pluggable Remote Helpers
|
||||
|
||||
For custom proxying or authentication, Repo supports pluggable remote helpers.
|
||||
You can declare a helper binary via the optional `helper` attribute on the
|
||||
`<manifest-server>` element.
|
||||
|
||||
If the `helper` attribute is present in `<manifest-server>`:
|
||||
1. Repo searches your system `PATH` for the specified helper binary (e.g.,
|
||||
`repo-remote-sso`). If the helper cannot be found in `PATH`, Repo will raise
|
||||
a `SmartSyncError` and abort the sync.
|
||||
2. The helper is executed with the manifest server URL as its first argument:
|
||||
```bash
|
||||
<helper-binary-name> <url>
|
||||
```
|
||||
3. The helper must output a single-line JSON object on stdout and exit:
|
||||
* On success, return `status: "ok"` and a loopback proxy URL (including
|
||||
scheme, e.g., `http://127.0.0.1:999`):
|
||||
```json
|
||||
{"status": "ok", "message": "http://127.0.0.1:999"}
|
||||
```
|
||||
* On failure, return `status: "error"` and a detailed error message:
|
||||
```json
|
||||
{"status": "error", "message": "unauthorized"}
|
||||
```
|
||||
4. Repo routes the XML-RPC request through the returned proxy address.
|
||||
|
||||
**Timeout Constraint**: The remote helper must complete and exit within 10
|
||||
seconds. If it hangs or exceeds this limit, Repo will force-terminate (`kill`)
|
||||
the process and abort the synchronization.
|
||||
|
||||
## Credentials
|
||||
|
||||
Credentials may be specified directly in typical `username:password`
|
||||
|
||||
+7
-1
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2026" "repo manifest" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo manifest" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo manifest - manual page for repo manifest
|
||||
.SH SYNOPSIS
|
||||
@@ -138,6 +138,7 @@ include*)>
|
||||
.IP
|
||||
<!ELEMENT manifest\-server EMPTY>
|
||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest\-server helper CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
@@ -347,6 +348,11 @@ specify the URL of a manifest server, which is an XML RPC service.
|
||||
.PP
|
||||
See the [smart sync documentation](./smart\-sync.md) for more details.
|
||||
.PP
|
||||
Attribute `url`: The URL of the manifest server.
|
||||
.PP
|
||||
Attribute `helper`: Optional name of a remote helper binary to execute to
|
||||
resolve proxying or authentication for the manifest server.
|
||||
.PP
|
||||
Element submanifest
|
||||
.PP
|
||||
One or more submanifest elements may be specified. Each element describes a
|
||||
|
||||
@@ -651,6 +651,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if self._manifest_server:
|
||||
e = doc.createElement("manifest-server")
|
||||
e.setAttribute("url", self._manifest_server)
|
||||
if self._manifest_server_helper:
|
||||
e.setAttribute("helper", self._manifest_server_helper)
|
||||
root.appendChild(e)
|
||||
root.appendChild(doc.createTextNode(""))
|
||||
|
||||
@@ -999,6 +1001,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
self._Load()
|
||||
return self._manifest_server
|
||||
|
||||
@property
|
||||
def manifest_server_helper(self):
|
||||
self._Load()
|
||||
return self._manifest_server_helper
|
||||
|
||||
@property
|
||||
def CloneBundle(self):
|
||||
clone_bundle = self.manifestProject.clone_bundle
|
||||
@@ -1157,6 +1164,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
self._notice = None
|
||||
self.branch = None
|
||||
self._manifest_server = None
|
||||
self._manifest_server_helper = None
|
||||
|
||||
def Load(self):
|
||||
"""Read the manifest into memory."""
|
||||
@@ -1426,11 +1434,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
for node in itertools.chain(*node_list):
|
||||
if node.nodeName == "manifest-server":
|
||||
url = self._reqatt(node, "url")
|
||||
helper = node.getAttribute("helper") or None
|
||||
if self._manifest_server is not None:
|
||||
raise ManifestParseError(
|
||||
"duplicate manifest-server in %s" % (self.manifestFile)
|
||||
)
|
||||
self._manifest_server = url
|
||||
self._manifest_server_helper = helper
|
||||
|
||||
def recursively_add_projects(project):
|
||||
projects = self._projects.setdefault(project.name, [])
|
||||
|
||||
+147
-30
@@ -23,6 +23,8 @@ import netrc
|
||||
import optparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
@@ -1738,16 +1740,104 @@ later is required to fix a server side protocol bug.
|
||||
|
||||
return True
|
||||
|
||||
def _SmartSyncSetup(self, opt, smart_sync_manifest_path, manifest):
|
||||
if not manifest.manifest_server:
|
||||
raise SmartSyncError(
|
||||
"error: cannot smart sync: no manifest server defined in "
|
||||
"manifest"
|
||||
)
|
||||
def _ResolveManifestServerTransport(self, opt, manifest):
|
||||
"""Resolves the manifest server URL and transport.
|
||||
|
||||
Returns:
|
||||
Tuple[str, xmlrpc.client.Transport]: The resolved server URL and
|
||||
transport.
|
||||
|
||||
Raises:
|
||||
SmartSyncError: If resolution fails (e.g. helper missing, helper
|
||||
error, unsupported scheme).
|
||||
"""
|
||||
manifest_server = manifest.manifest_server
|
||||
if not opt.quiet:
|
||||
print("Using manifest server %s" % manifest_server)
|
||||
helper_binary = manifest.manifest_server_helper
|
||||
|
||||
if helper_binary:
|
||||
if not shutil.which(helper_binary):
|
||||
raise SmartSyncError(
|
||||
f"error: helper binary '{helper_binary}' declared in "
|
||||
"manifest was not found in your PATH."
|
||||
)
|
||||
|
||||
if not opt.quiet:
|
||||
print(f"Using remote helper {helper_binary}")
|
||||
|
||||
p = None
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
[helper_binary, manifest_server],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
stdout, stderr = p.communicate(timeout=10)
|
||||
output = stdout.strip()
|
||||
stderr_content = stderr.strip() if stderr else ""
|
||||
except subprocess.TimeoutExpired as e:
|
||||
err_msg = f"helper {helper_binary} timed out after 10 seconds"
|
||||
timeout_stderr = e.stderr.strip() if e.stderr else ""
|
||||
if timeout_stderr:
|
||||
err_msg += f". Stderr: {timeout_stderr}"
|
||||
raise SmartSyncError(err_msg)
|
||||
except OSError as e:
|
||||
raise SmartSyncError(
|
||||
"failed to start or communicate with helper "
|
||||
f"{helper_binary}: {e}"
|
||||
)
|
||||
finally:
|
||||
if p and p.poll() is None:
|
||||
p.kill()
|
||||
p.wait()
|
||||
|
||||
if p.returncode != 0:
|
||||
err_msg = (
|
||||
f"helper {helper_binary} exited with exit code "
|
||||
f"{p.returncode}."
|
||||
)
|
||||
if stderr_content:
|
||||
err_msg += f" Stderr: {stderr_content}"
|
||||
raise SmartSyncError(err_msg)
|
||||
|
||||
try:
|
||||
res = json.loads(output)
|
||||
status = res.get("status")
|
||||
msg = res.get("message")
|
||||
except json.JSONDecodeError as e:
|
||||
err_msg = (
|
||||
f"failed to parse JSON from helper {helper_binary}: {e}. "
|
||||
f"Output was: {output}"
|
||||
)
|
||||
if stderr_content:
|
||||
err_msg += f"\nStderr was: {stderr_content}"
|
||||
raise SmartSyncError(err_msg)
|
||||
|
||||
if status != "ok":
|
||||
err_msg = f"helper {helper_binary} returned error: {msg}"
|
||||
if stderr_content:
|
||||
err_msg += f"\nStderr was: {stderr_content}"
|
||||
raise SmartSyncError(err_msg)
|
||||
|
||||
proxy_url = msg
|
||||
transport = PersistentTransport(manifest_server, proxy=proxy_url)
|
||||
server_url = manifest_server
|
||||
if server_url.startswith("persistent-"):
|
||||
server_url = server_url[len("persistent-") :]
|
||||
return server_url, transport
|
||||
|
||||
# Fallback path if helper isn't specified
|
||||
scheme = urllib.parse.urlparse(manifest_server).scheme
|
||||
if scheme not in (
|
||||
"http",
|
||||
"https",
|
||||
"persistent-http",
|
||||
"persistent-https",
|
||||
):
|
||||
raise SmartSyncError(
|
||||
f"error: unsupported manifest server scheme '{scheme}'."
|
||||
)
|
||||
|
||||
if "@" not in manifest_server:
|
||||
username = None
|
||||
@@ -1782,20 +1872,34 @@ later is required to fix a server side protocol bug.
|
||||
)
|
||||
|
||||
transport = PersistentTransport(manifest_server)
|
||||
if manifest_server.startswith("persistent-"):
|
||||
manifest_server = manifest_server[len("persistent-") :]
|
||||
server_url = manifest_server
|
||||
if server_url.startswith("persistent-"):
|
||||
server_url = server_url[len("persistent-") :]
|
||||
|
||||
return server_url, transport
|
||||
|
||||
def _SmartSyncSetup(self, opt, smart_sync_manifest_path, manifest):
|
||||
if not manifest.manifest_server:
|
||||
raise SmartSyncError(
|
||||
"error: cannot smart sync: no manifest server defined in "
|
||||
"manifest"
|
||||
)
|
||||
|
||||
if not opt.quiet:
|
||||
print("Using manifest server %s" % manifest.manifest_server)
|
||||
|
||||
server_url, transport = self._ResolveManifestServerTransport(
|
||||
opt, manifest
|
||||
)
|
||||
|
||||
# Changes in behavior should update docs/smart-sync.md accordingly.
|
||||
try:
|
||||
server = xmlrpc.client.Server(manifest_server, transport=transport)
|
||||
server = xmlrpc.client.Server(server_url, transport=transport)
|
||||
if opt.smart_sync:
|
||||
branch = self._GetBranch(manifest.manifestProject)
|
||||
|
||||
target = None
|
||||
if "SYNC_TARGET" in os.environ:
|
||||
target = os.environ["SYNC_TARGET"]
|
||||
[success, manifest_str] = server.GetApprovedManifest(
|
||||
branch, target
|
||||
)
|
||||
elif (
|
||||
"TARGET_PRODUCT" in os.environ
|
||||
and "TARGET_BUILD_VARIANT" in os.environ
|
||||
@@ -1806,9 +1910,6 @@ later is required to fix a server side protocol bug.
|
||||
os.environ["TARGET_RELEASE"],
|
||||
os.environ["TARGET_BUILD_VARIANT"],
|
||||
)
|
||||
[success, manifest_str] = server.GetApprovedManifest(
|
||||
branch, target
|
||||
)
|
||||
elif (
|
||||
"TARGET_PRODUCT" in os.environ
|
||||
and "TARGET_BUILD_VARIANT" in os.environ
|
||||
@@ -1817,6 +1918,8 @@ later is required to fix a server side protocol bug.
|
||||
os.environ["TARGET_PRODUCT"],
|
||||
os.environ["TARGET_BUILD_VARIANT"],
|
||||
)
|
||||
|
||||
if target:
|
||||
[success, manifest_str] = server.GetApprovedManifest(
|
||||
branch, target
|
||||
)
|
||||
@@ -1838,25 +1941,34 @@ later is required to fix a server side protocol bug.
|
||||
aggregate_errors=[e],
|
||||
)
|
||||
self._ReloadManifest(manifest_name, manifest)
|
||||
else:
|
||||
raise SmartSyncError(
|
||||
"error: manifest server RPC call failed: %s" % manifest_str
|
||||
)
|
||||
return manifest_name
|
||||
|
||||
raise SmartSyncError(
|
||||
"error: manifest server RPC call failed: %s" % manifest_str
|
||||
)
|
||||
except (OSError, xmlrpc.client.Fault) as e:
|
||||
if manifest.manifest_server_helper:
|
||||
raise SmartSyncError(
|
||||
"error: failed to communicate with manifest server via "
|
||||
f"helper: {e}"
|
||||
)
|
||||
raise SmartSyncError(
|
||||
"error: cannot connect to manifest server %s:\n%s"
|
||||
% (manifest.manifest_server, e),
|
||||
aggregate_errors=[e],
|
||||
)
|
||||
except xmlrpc.client.ProtocolError as e:
|
||||
if manifest.manifest_server_helper:
|
||||
raise SmartSyncError(
|
||||
"error: failed to communicate with manifest server via "
|
||||
f"helper: {e}"
|
||||
)
|
||||
raise SmartSyncError(
|
||||
"error: cannot connect to manifest server %s:\n%d %s"
|
||||
% (manifest.manifest_server, e.errcode, e.errmsg),
|
||||
aggregate_errors=[e],
|
||||
)
|
||||
|
||||
return manifest_name
|
||||
|
||||
def _UpdateAllManifestProjects(self, opt, mp, manifest_name, errors):
|
||||
"""Fetch & update the local manifest project.
|
||||
|
||||
@@ -3094,14 +3206,15 @@ class LocalSyncState:
|
||||
# request to request like the normal transport, the real url
|
||||
# is passed during initialization.
|
||||
class PersistentTransport(xmlrpc.client.Transport):
|
||||
def __init__(self, orig_host):
|
||||
def __init__(self, orig_host, proxy=None):
|
||||
super().__init__()
|
||||
self.orig_host = orig_host
|
||||
self.proxy = proxy
|
||||
|
||||
def request(self, host, handler, request_body, verbose=False):
|
||||
with GetUrlCookieFile(self.orig_host, not verbose) as (
|
||||
cookiefile,
|
||||
proxy,
|
||||
cookie_proxy,
|
||||
):
|
||||
# Python doesn't understand cookies with the #HttpOnly_ prefix
|
||||
# Since we're only using them for HTTP, copy the file temporarily,
|
||||
@@ -3127,10 +3240,11 @@ class PersistentTransport(xmlrpc.client.Transport):
|
||||
else:
|
||||
cookiejar = cookielib.CookieJar()
|
||||
|
||||
active_proxy = self.proxy or cookie_proxy
|
||||
proxyhandler = urllib.request.ProxyHandler
|
||||
if proxy:
|
||||
if active_proxy:
|
||||
proxyhandler = urllib.request.ProxyHandler(
|
||||
{"http": proxy, "https": proxy}
|
||||
{"http": active_proxy, "https": active_proxy}
|
||||
)
|
||||
|
||||
opener = urllib.request.build_opener(
|
||||
@@ -3143,13 +3257,16 @@ class PersistentTransport(xmlrpc.client.Transport):
|
||||
scheme = parse_results.scheme
|
||||
if scheme == "persistent-http":
|
||||
scheme = "http"
|
||||
if scheme == "persistent-https":
|
||||
elif scheme == "persistent-https":
|
||||
# If we're proxying through persistent-https, use http. The
|
||||
# proxy itself will do the https.
|
||||
if proxy:
|
||||
if active_proxy:
|
||||
scheme = "http"
|
||||
else:
|
||||
scheme = "https"
|
||||
elif scheme not in ("http", "https"):
|
||||
if active_proxy:
|
||||
scheme = "http"
|
||||
|
||||
# Parse out any authentication information using the base class.
|
||||
host, extra_headers, _ = self.get_host_info(parse_results.netloc)
|
||||
|
||||
@@ -1514,3 +1514,232 @@ class UpdateAllManifestProjectsTests(unittest.TestCase):
|
||||
)
|
||||
mock_sync_to_rev.assert_not_called()
|
||||
mock_update_manifest.assert_called_once()
|
||||
|
||||
|
||||
class TestSmartSyncSetupRemoteHelper(unittest.TestCase):
|
||||
"""Tests for _SmartSyncSetup with remote helpers."""
|
||||
|
||||
def setUp(self):
|
||||
self.cmd = sync.Sync()
|
||||
self.opt = mock.MagicMock()
|
||||
self.opt.quiet = False
|
||||
self.opt.smart_sync = True
|
||||
self.manifest = mock.MagicMock()
|
||||
self.smart_sync_manifest_path = "/fake/path/to/manifest.xml"
|
||||
|
||||
@mock.patch("shutil.which")
|
||||
@mock.patch("subprocess.Popen")
|
||||
@mock.patch("xmlrpc.client.Server")
|
||||
@mock.patch("subcmds.sync.PersistentTransport")
|
||||
def test_smart_sync_setup_with_helper(
|
||||
self, mock_transport_class, mock_server_class, mock_popen, mock_which
|
||||
):
|
||||
"""Test _SmartSyncSetup when a helper is present and succeeds."""
|
||||
import subprocess
|
||||
|
||||
self.manifest.manifest_server = (
|
||||
"persistent-https://android-smartsync.corp.google.com/"
|
||||
"manifestserver"
|
||||
)
|
||||
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||
|
||||
# Mock subprocess to return a JSON with status ok and proxy address
|
||||
mock_process = mock.MagicMock()
|
||||
mock_process.communicate.return_value = (
|
||||
'{"status":"ok","message":"http://127.0.0.1:999"}\n',
|
||||
"",
|
||||
)
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
# Mock XML-RPC server call
|
||||
mock_server = mock.MagicMock()
|
||||
mock_server.GetApprovedManifest.return_value = [
|
||||
True,
|
||||
"<manifest></manifest>",
|
||||
]
|
||||
mock_server_class.return_value = mock_server
|
||||
|
||||
# Mock manifest project branch
|
||||
self.cmd._GetBranch = mock.MagicMock(return_value="main")
|
||||
self.cmd._ReloadManifest = mock.MagicMock()
|
||||
|
||||
# Mock open to avoid writing to disk
|
||||
with mock.patch("builtins.open", mock.mock_open()):
|
||||
manifest_name = self.cmd._SmartSyncSetup(
|
||||
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||
)
|
||||
|
||||
# Assertions
|
||||
mock_which.assert_called_once_with("repo-remote-sso")
|
||||
mock_popen.assert_called_once_with(
|
||||
["repo-remote-sso", self.manifest.manifest_server],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# Verify transport was created with the proxy returned by helper (with
|
||||
# http:// prepended)
|
||||
mock_transport_class.assert_called_once_with(
|
||||
self.manifest.manifest_server, proxy="http://127.0.0.1:999"
|
||||
)
|
||||
|
||||
# Verify Server was created with the same URL, with persistent- stripped
|
||||
mock_server_class.assert_called_once_with(
|
||||
"https://android-smartsync.corp.google.com/manifestserver",
|
||||
transport=mock_transport_class.return_value,
|
||||
)
|
||||
|
||||
self.assertEqual(manifest_name, "manifest.xml")
|
||||
|
||||
@mock.patch("shutil.which")
|
||||
@mock.patch("subprocess.Popen")
|
||||
def test_smart_sync_setup_helper_error(self, mock_popen, mock_which):
|
||||
"""Test _SmartSyncSetup when helper returns an error status."""
|
||||
self.manifest.manifest_server = (
|
||||
"http://android-smartsync.corp.google.com/manifestserver"
|
||||
)
|
||||
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||
|
||||
# Mock subprocess to return a JSON with status error
|
||||
mock_process = mock.MagicMock()
|
||||
mock_process.communicate.return_value = (
|
||||
'{"status":"error","message":"uplink-helper failed"}\n',
|
||||
"",
|
||||
)
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
with self.assertRaises(sync.SmartSyncError) as context:
|
||||
self.cmd._SmartSyncSetup(
|
||||
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"helper repo-remote-sso returned error: uplink-helper failed",
|
||||
str(context.exception),
|
||||
)
|
||||
|
||||
@mock.patch("shutil.which")
|
||||
def test_smart_sync_setup_missing_declared_helper(self, mock_which):
|
||||
"""Test _SmartSyncSetup when helper declared in manifest is missing."""
|
||||
self.manifest.manifest_server = (
|
||||
"http://android-smartsync.corp.google.com/manifestserver"
|
||||
)
|
||||
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||
mock_which.return_value = None
|
||||
|
||||
with self.assertRaises(sync.SmartSyncError) as context:
|
||||
self.cmd._SmartSyncSetup(
|
||||
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"helper binary 'repo-remote-sso' declared in manifest was not "
|
||||
"found",
|
||||
str(context.exception),
|
||||
)
|
||||
|
||||
@mock.patch("shutil.which")
|
||||
@mock.patch("subprocess.Popen")
|
||||
def test_smart_sync_setup_helper_exit_code_error(
|
||||
self, mock_popen, mock_which
|
||||
):
|
||||
"""Test _SmartSyncSetup when helper exits with non-zero and stderr."""
|
||||
self.manifest.manifest_server = (
|
||||
"http://android-smartsync.corp.google.com/manifestserver"
|
||||
)
|
||||
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||
|
||||
# Mock subprocess: exit code 1, stderr, and no JSON on stdout
|
||||
mock_process = mock.MagicMock()
|
||||
mock_process.communicate.return_value = (
|
||||
"",
|
||||
"internal binary error occurred\n",
|
||||
)
|
||||
mock_process.returncode = 1
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
with self.assertRaises(sync.SmartSyncError) as context:
|
||||
self.cmd._SmartSyncSetup(
|
||||
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"helper repo-remote-sso exited with exit code 1. "
|
||||
"Stderr: internal binary error occurred",
|
||||
str(context.exception),
|
||||
)
|
||||
|
||||
@mock.patch("shutil.which")
|
||||
@mock.patch("subprocess.Popen")
|
||||
def test_smart_sync_setup_helper_json_decode_error_with_stderr(
|
||||
self, mock_popen, mock_which
|
||||
):
|
||||
"""Test _SmartSyncSetup when helper returns invalid JSON and stderr."""
|
||||
self.manifest.manifest_server = (
|
||||
"http://android-smartsync.corp.google.com/manifestserver"
|
||||
)
|
||||
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||
|
||||
mock_process = mock.MagicMock()
|
||||
mock_process.communicate.return_value = (
|
||||
"not a json",
|
||||
"some warning messages\n",
|
||||
)
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
with self.assertRaises(sync.SmartSyncError) as context:
|
||||
self.cmd._SmartSyncSetup(
|
||||
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"failed to parse JSON from helper repo-remote-sso",
|
||||
str(context.exception),
|
||||
)
|
||||
self.assertIn(
|
||||
"Stderr was: some warning messages",
|
||||
str(context.exception),
|
||||
)
|
||||
|
||||
@mock.patch("shutil.which")
|
||||
@mock.patch("subprocess.Popen")
|
||||
def test_smart_sync_setup_helper_error_with_stderr(
|
||||
self, mock_popen, mock_which
|
||||
):
|
||||
"""Test _SmartSyncSetup when helper returns error status and stderr."""
|
||||
self.manifest.manifest_server = (
|
||||
"http://android-smartsync.corp.google.com/manifestserver"
|
||||
)
|
||||
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||
|
||||
mock_process = mock.MagicMock()
|
||||
mock_process.communicate.return_value = (
|
||||
'{"status":"error","message":"uplink-helper failed"}\n',
|
||||
"debugging logs\n",
|
||||
)
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
with self.assertRaises(sync.SmartSyncError) as context:
|
||||
self.cmd._SmartSyncSetup(
|
||||
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"helper repo-remote-sso returned error: uplink-helper failed",
|
||||
str(context.exception),
|
||||
)
|
||||
self.assertIn(
|
||||
"Stderr was: debugging logs",
|
||||
str(context.exception),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user