| # Copyright 2012 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Unittests for GerritHelper. |
| |
| Most of the tests in this file reach out to a staging/test Gerrit server. By |
| default, this server is t3st-chr0m3-review.googlesource.com. These tests will |
| be skipped unless run_tests is passed the '--network' arg: |
| $ ./run_tests --network -- lib/gerrit_unittest.py |
| """ |
| |
| import collections |
| import http.client |
| import http.cookiejar |
| import io |
| import json |
| import os |
| import re |
| import shutil |
| import stat |
| from unittest import mock |
| import urllib.parse |
| |
| from chromite.lib import config_lib |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_test_lib |
| from chromite.lib import gerrit |
| from chromite.lib import git |
| from chromite.lib import gob_util |
| from chromite.lib import osutils |
| |
| |
| class GerritTestCase(cros_test_lib.MockTempDirTestCase): |
| """Test class for tests that interact with a Gerrit server. |
| |
| Configured by default to use a specially-configured test Gerrit server at |
| t3st-chr0m3(-review).googlesource.com. The test server configuration may be |
| altered by setting the following environment variables from the parent |
| process: |
| CROS_TEST_GIT_HOST: host name for git operations; defaults to |
| t3st-chr0me.googlesource.com. |
| CROS_TEST_GERRIT_HOST: host name for Gerrit operations; defaults to |
| t3st-chr0me-review.googlesource.com. |
| CROS_TEST_COOKIES_PATH: path to a cookies.txt file to use for git/Gerrit |
| requests; defaults to ~/.gitcookies. |
| CROS_TEST_COOKIE_NAMES: comma-separated list of cookie names from |
| CROS_TEST_COOKIES_PATH to set on requests; defaults to none. The |
| current implementation only sends cookies matching the exact host |
| name and the empty path ("/"). |
| """ |
| |
| # pylint: disable=protected-access |
| |
| TEST_USERNAME = "test-username" |
| TEST_EMAIL = "test-username@test.org" |
| |
| GerritInstance = collections.namedtuple( |
| "GerritInstance", |
| [ |
| "cookie_names", |
| "cookies_path", |
| "gerrit_host", |
| "gerrit_url", |
| "git_host", |
| "git_url", |
| "project_prefix", |
| ], |
| ) |
| |
| def _create_gerrit_instance(self, tmp_dir): |
| default_host = "t3st-chr0m3" |
| git_host = os.environ.get( |
| "CROS_TEST_GIT_HOST", constants.GOB_HOST % default_host |
| ) |
| gerrit_host = os.environ.get( |
| "CROS_TEST_GERRIT_HOST", "%s-review.googlesource.com" % default_host |
| ) |
| project_prefix = "test-%s/" % (cros_build_lib.GetRandomString(),) |
| cookies_path = os.environ.get( |
| "CROS_TEST_COOKIES_PATH", str(constants.GITCOOKIES_PATH) |
| ) |
| # "o" is the cookie name that GoB uses in its instructions. |
| cookie_names_str = os.environ.get("CROS_TEST_COOKIE_NAMES", "o") |
| cookie_names = {c for c in cookie_names_str.split(",") if c} |
| |
| tmpcookies_path = os.path.join(tmp_dir, ".gitcookies") |
| if os.path.exists(cookies_path): |
| shutil.copy(cookies_path, tmpcookies_path) |
| else: |
| osutils.Touch(tmpcookies_path) |
| |
| return self.GerritInstance( |
| cookie_names=cookie_names, |
| cookies_path=tmpcookies_path, |
| gerrit_host=gerrit_host, |
| gerrit_url="https://%s/" % gerrit_host, |
| git_host=git_host, |
| git_url="https://%s/" % git_host, |
| project_prefix=project_prefix, |
| ) |
| |
| def setUp(self) -> None: |
| """Sets up the gerrit instances in a class-specific temp dir.""" |
| self.saved_params = {} |
| os.environ["HOME"] = self.tempdir |
| |
| # Create gerrit instance. |
| gi = self.gerrit_instance = self._create_gerrit_instance(self.tempdir) |
| |
| # This --global will use our tempdir $HOME we set above, not the real |
| # ~/. |
| cros_build_lib.dbg_run( |
| ["git", "config", "--global", "http.cookiefile", gi.cookies_path], |
| capture_output=True, |
| ) |
| |
| # If you're seeing "does not look like a Netscape format cookies file" |
| # errors here, make sure the first line in your gitcookies file is: |
| # "# HTTP Cookie File" |
| # TODO(b/210490942): Detect and handle this automatically. |
| jar = http.cookiejar.MozillaCookieJar(gi.cookies_path) |
| jar.load(ignore_expires=True) |
| |
| def GetCookies(host, _path): |
| return dict( |
| (c.name, urllib.parse.unquote(c.value)) |
| for c in jar |
| if c.domain == host |
| and c.path == "/" |
| and c.name in gi.cookie_names |
| ) |
| |
| self.PatchObject(gob_util, "GetCookies", GetCookies) |
| |
| site_params = config_lib.GetSiteParams() |
| |
| # Make all chromite code point to the test server. |
| self.patched_params = { |
| "EXTERNAL_GOB_HOST": gi.git_host, |
| "EXTERNAL_GERRIT_HOST": gi.gerrit_host, |
| "EXTERNAL_GOB_URL": gi.git_url, |
| "EXTERNAL_GERRIT_URL": gi.gerrit_url, |
| "INTERNAL_GOB_HOST": gi.git_host, |
| "INTERNAL_GERRIT_HOST": gi.gerrit_host, |
| "INTERNAL_GOB_URL": gi.git_url, |
| "INTERNAL_GERRIT_URL": gi.gerrit_url, |
| "AOSP_GOB_HOST": gi.git_host, |
| "AOSP_GERRIT_HOST": gi.gerrit_host, |
| "AOSP_GOB_URL": gi.git_url, |
| "AOSP_GERRIT_URL": gi.gerrit_url, |
| "MANIFEST_URL": "%s/%s" |
| % ( |
| gi.git_url, |
| site_params.MANIFEST_PROJECT, |
| ), |
| "MANIFEST_INT_URL": "%s/%s" |
| % ( |
| gi.git_url, |
| site_params.MANIFEST_INT_PROJECT, |
| ), |
| "GIT_REMOTES": { |
| site_params.EXTERNAL_REMOTE: gi.gerrit_url, |
| site_params.INTERNAL_REMOTE: gi.gerrit_url, |
| site_params.CHROMIUM_REMOTE: gi.gerrit_url, |
| site_params.CHROME_REMOTE: gi.gerrit_url, |
| }, |
| } |
| |
| for k in self.patched_params.keys(): |
| self.saved_params[k] = site_params.get(k) |
| |
| site_params.update(self.patched_params) |
| |
| def tearDown(self) -> None: |
| # Restore the 'patched' site parameters. |
| site_params = config_lib.GetSiteParams() |
| site_params.update(self.saved_params) |
| |
| def createProject( |
| self, |
| suffix, |
| description="Test project", |
| owners=None, |
| submit_type="CHERRY_PICK", |
| ): |
| """Create a project on the test gerrit server.""" |
| name = self.gerrit_instance.project_prefix + suffix |
| body = { |
| "description": description, |
| "submit_type": submit_type, |
| "branches": ["main"], |
| } |
| if owners is not None: |
| body["owners"] = owners |
| path = "projects/%s" % urllib.parse.quote(name, "") |
| response = gob_util.CreateHttpConn( |
| self.gerrit_instance.gerrit_host, path, reqtype="PUT", body=body |
| ) |
| self.assertEqual( |
| 201, response.status, "Expected 201, got %s" % response.status |
| ) |
| s = io.BytesIO(response.read()) |
| self.assertEqual(b")]}'", s.readline().rstrip()) |
| jmsg = json.load(s) |
| self.assertEqual(name, jmsg["name"]) |
| return name |
| |
| def _CloneProject(self, name, path): |
| """Clone a project from the test gerrit server.""" |
| root = os.path.dirname(path) |
| osutils.SafeMakedirs(root) |
| url = "%s://%s/%s" % ( |
| gob_util.GIT_PROTOCOL, |
| self.gerrit_instance.git_host, |
| name, |
| ) |
| git.RunGit(root, ["clone", url, path]) |
| # Install commit-msg hook. |
| hook_path = os.path.join(path, ".git", "hooks", "commit-msg") |
| hook_cmd = [ |
| "curl", |
| "-n", |
| "-o", |
| hook_path, |
| "-b", |
| self.gerrit_instance.cookies_path, |
| ] |
| hook_cmd.append( |
| "https://%s/a/tools/hooks/commit-msg" |
| % self.gerrit_instance.gerrit_host |
| ) |
| cros_build_lib.dbg_run(hook_cmd, capture_output=True) |
| os.chmod(hook_path, stat.S_IRWXU) |
| # Set git identity to test account |
| cros_build_lib.dbg_run( |
| ["git", "config", "user.email", self.TEST_EMAIL], |
| cwd=path, |
| capture_output=True, |
| ) |
| return path |
| |
| def cloneProject(self, name, path=None): |
| """Clone a project from the test gerrit server.""" |
| if path is None: |
| path = os.path.basename(name) |
| if path.endswith(".git"): |
| path = path[:-4] |
| path = os.path.join(self.tempdir, path) |
| return self._CloneProject(name, path) |
| |
| @classmethod |
| def _CreateCommit( |
| cls, clone_path, filename=None, msg=None, text=None, amend=False |
| ): |
| """Create a commit in the given git checkout. |
| |
| Args: |
| clone_path: The directory on disk of the git clone. |
| filename: The name of the file to write. Optional. |
| msg: The commit message. Optional. |
| text: The text to append to the file. Optional. |
| amend: Whether to amend an existing patch. If set, we will amend the |
| HEAD commit in the checkout and upload that patch. |
| |
| Returns: |
| (sha1, changeid) of the new commit. |
| """ |
| if not filename: |
| filename = "test-file.txt" |
| if not msg: |
| msg = "Test Message" |
| if not text: |
| text = "Another day, another dollar." |
| fpath = os.path.join(clone_path, filename) |
| osutils.WriteFile(fpath, "%s\n" % text, mode="a") |
| cros_build_lib.dbg_run( |
| ["git", "add", filename], cwd=clone_path, capture_output=True |
| ) |
| cmd = ["git", "commit"] |
| cmd += ["--amend", "-C", "HEAD"] if amend else ["-m", msg] |
| cros_build_lib.dbg_run(cmd, cwd=clone_path, capture_output=True) |
| return cls._GetCommit(clone_path) |
| |
| def createCommit( |
| self, clone_path, filename=None, msg=None, text=None, amend=False |
| ): |
| """Create a commit in the given git checkout. |
| |
| Args: |
| clone_path: The directory on disk of the git clone. |
| filename: The name of the file to write. Optional. |
| msg: The commit message. Optional. |
| text: The text to append to the file. Optional. |
| amend: Whether to amend an existing patch. If set, we will amend the |
| HEAD commit in the checkout and upload that patch. |
| """ |
| clone_path = os.path.join(self.tempdir, clone_path) |
| return self._CreateCommit(clone_path, filename, msg, text, amend) |
| |
| @staticmethod |
| def _GetCommit(clone_path, ref="HEAD"): |
| log_proc = cros_build_lib.run( |
| ["git", "log", "-n", "1", ref], |
| cwd=clone_path, |
| print_cmd=False, |
| capture_output=True, |
| encoding="utf-8", |
| ) |
| sha1 = None |
| change_id = None |
| for line in log_proc.stdout.splitlines(): |
| match = re.match(r"^commit ([0-9a-fA-F]{40})$", line) |
| if match: |
| sha1 = match.group(1) |
| continue |
| match = re.match(r"^\s+Change-Id:\s*(\S+)$", line) |
| if match: |
| change_id = match.group(1) |
| continue |
| return (sha1, change_id) |
| |
| def getCommit(self, clone_path, ref="HEAD"): |
| """Get the sha1 and change-id for the head commit in a git checkout.""" |
| clone_path = os.path.join(self.tempdir, clone_path) |
| (sha1, change_id) = self._GetCommit(clone_path, ref) |
| self.assertTrue(sha1) |
| self.assertTrue(change_id) |
| return (sha1, change_id) |
| |
| @staticmethod |
| def _UploadChange(clone_path, branch="main", remote="origin") -> None: |
| cros_build_lib.dbg_run( |
| ["git", "push", remote, "HEAD:refs/for/%s" % branch], |
| cwd=clone_path, |
| capture_output=True, |
| ) |
| |
| def uploadChange(self, clone_path, branch="main", remote="origin") -> None: |
| """Create a gerrit CL from the HEAD of a git checkout.""" |
| clone_path = os.path.join(self.tempdir, clone_path) |
| self._UploadChange(clone_path, branch, remote) |
| |
| @staticmethod |
| def _PushBranch(clone_path, branch="main") -> None: |
| cros_build_lib.dbg_run( |
| ["git", "push", "origin", "HEAD:refs/heads/%s" % branch], |
| cwd=clone_path, |
| capture_output=True, |
| ) |
| |
| def pushBranch(self, clone_path, branch="main") -> None: |
| """Push a branch directly to gerrit, bypassing code review.""" |
| clone_path = os.path.join(self.tempdir, clone_path) |
| self._PushBranch(clone_path, branch) |
| |
| def createAccount( |
| self, |
| name="Test User", |
| email="test-user@test.org", |
| password=None, |
| groups=None, |
| ) -> None: |
| """Create a new user account on gerrit.""" |
| username = urllib.parse.quote(email.partition("@")[0]) |
| path = "accounts/%s" % username |
| body = { |
| "name": name, |
| "email": email, |
| } |
| |
| if password: |
| body["http_password"] = password |
| if groups: |
| if isinstance(groups, str): |
| groups = [groups] |
| body["groups"] = groups |
| response = gob_util.CreateHttpConn( |
| self.gerrit_instance.gerrit_host, path, reqtype="PUT", body=body |
| ) |
| self.assertEqual(201, response.status) |
| s = io.BytesIO(response.read()) |
| self.assertEqual(b")]}'", s.readline().rstrip()) |
| jmsg = json.load(s) |
| self.assertEqual(email, jmsg["email"]) |
| |
| |
| @cros_test_lib.pytestmark_network_test |
| class GerritHelperTest(GerritTestCase): |
| """Unittests for GerritHelper.""" |
| |
| def _GetHelper(self, remote=config_lib.GetSiteParams().EXTERNAL_REMOTE): |
| return gerrit.GetGerritHelper(remote) |
| |
| def createPatch(self, clone_path, project, remote="origin", **kwargs): |
| """Create a patch in the given git checkout and upload it to gerrit. |
| |
| Args: |
| clone_path: The directory on disk of the git clone. |
| project: The associated project. |
| remote: The remote to upload changes to. |
| **kwargs: Additional keyword arguments to pass to createCommit. |
| |
| Returns: |
| A GerritPatch object. |
| """ |
| (revision, changeid) = self.createCommit(clone_path, **kwargs) |
| helper = self._GetHelper() |
| gpatch = helper.CreateGerritPatch( |
| clone_path, remote, "main", project=project |
| ) |
| self.assertEqual(gpatch.change_id, changeid) |
| self.assertEqual(gpatch.revision, revision) |
| return gpatch |
| |
| def testSimpleQuery(self) -> None: |
| """Create and query one independent and three dependent changes.""" |
| project = self.createProject("testProject") |
| clone_path = self.cloneProject(project) |
| (head_sha1, head_changeid) = self.createCommit(clone_path) |
| for idx in range(3): |
| cros_build_lib.dbg_run( |
| ["git", "checkout", head_sha1], |
| cwd=clone_path, |
| capture_output=True, |
| ) |
| self.createCommit(clone_path, filename="test-file-%d.txt" % idx) |
| self.uploadChange(clone_path) |
| helper = self._GetHelper() |
| changes = helper.Query(owner="self", project=project) |
| self.assertEqual(len(changes), 4) |
| changes = helper.Query(head_changeid, project=project, branch="main") |
| self.assertEqual(len(changes), 1) |
| self.assertEqual(changes[0].change_id, head_changeid) |
| self.assertEqual(changes[0].sha1, head_sha1) |
| change = helper.QuerySingleRecord( |
| head_changeid, project=project, branch="main" |
| ) |
| self.assertTrue(change) |
| self.assertEqual(change.change_id, head_changeid) |
| self.assertEqual(change.sha1, head_sha1) |
| change = helper.GrabPatchFromGerrit(project, head_changeid, head_sha1) |
| self.assertTrue(change) |
| self.assertEqual(change.change_id, head_changeid) |
| self.assertEqual(change.sha1, head_sha1) |
| |
| @mock.patch.object(gerrit.GerritHelper, "_GERRIT_MAX_QUERY_RETURN", 2) |
| def testGerritQueryTruncation(self) -> None: |
| """Verify that we detect gerrit truncating our query, and handle it.""" |
| project = self.createProject("testProject") |
| clone_path = self.cloneProject(project) |
| # Using a shell loop is markedly faster than running a python loop. |
| num_changes = 5 |
| cmd = ( |
| "for ((i=0; i<%i; i=i+1)); do " |
| 'echo "Another day, another dollar." > test-file-$i.txt; ' |
| "git add test-file-$i.txt; " |
| 'git commit -m "Test commit $i."; ' |
| "done" % num_changes |
| ) |
| cros_build_lib.dbg_run( |
| cmd, shell=True, cwd=clone_path, capture_output=True |
| ) |
| self.uploadChange(clone_path) |
| helper = self._GetHelper() |
| changes = helper.Query(project=project) |
| self.assertEqual(len(changes), num_changes) |
| |
| def testIsChangeCommitted(self) -> None: |
| """Tests that we can parse a json to check if a change is committed.""" |
| project = self.createProject("testProject") |
| clone_path = self.cloneProject(project) |
| gpatch = self.createPatch(clone_path, project) |
| helper = self._GetHelper() |
| helper.SetReview(gpatch.gerrit_number, labels={"Code-Review": "+2"}) |
| helper.SubmitChange(gpatch) |
| self.assertTrue(helper.IsChangeCommitted(gpatch.gerrit_number)) |
| |
| gpatch = self.createPatch(clone_path, project) |
| self.assertFalse(helper.IsChangeCommitted(gpatch.gerrit_number)) |
| |
| def testGetLatestSHA1ForBranch(self) -> None: |
| """Verify we can query the tip-of-tree commit in a git repository.""" |
| project = self.createProject("testProject") |
| clone_path = self.cloneProject(project) |
| for _ in range(5): |
| (main_sha1, _) = self.createCommit(clone_path) |
| self.pushBranch(clone_path, "main") |
| for _ in range(5): |
| (testbranch_sha1, _) = self.createCommit(clone_path) |
| self.pushBranch(clone_path, "testbranch") |
| helper = self._GetHelper() |
| self.assertEqual( |
| helper.GetLatestSHA1ForBranch(project, "main"), main_sha1 |
| ) |
| self.assertEqual( |
| helper.GetLatestSHA1ForBranch(project, "testbranch"), |
| testbranch_sha1, |
| ) |
| |
| def testChangeEdit(self) -> None: |
| """Verify CreateChange & ChangeEdit can create CLs with changes.""" |
| project = self.createProject("testProject") |
| # Gerrit returns "Destination branch does not exist" errors if we don't |
| # push something onto the new project's branch. |
| clone_path = self.cloneProject(project) |
| self.createCommit(clone_path) |
| self.pushBranch(clone_path, "main") |
| helper = self._GetHelper() |
| # There should be no changes in our project that touch some_file. |
| file_path = "some_file" |
| self.assertEqual(len(helper.Query(project=project, path=file_path)), 0) |
| change = helper.CreateChange(project, "main", "Test Change", True) |
| helper.ChangeEdit(change.gerrit_number, file_path, "some file contents") |
| # After creating the change and adding a file modification, there should |
| # be a single change that touches some_file in our project. |
| self.assertEqual(len(helper.Query(project=project, path=file_path)), 1) |
| |
| def _ChooseReviewers(self): |
| # TODO(b/210507794): register some test accounts on test server. This |
| # fixed list of real IDs has a few problems, not limited to the |
| # following: |
| # * Some functions behave differently if the account is inactive; |
| # * Gerrit doesn't let you add yourself as a reviewer. So these |
| # accounts can't run the tests correctly. ;) |
| return ["dborowitz@google.com", "jrn@google.com"] |
| |
| def testSetAttentionSet(self) -> None: |
| """Verify that we can set the attention set on a CL.""" |
| project = self.createProject("testProject") |
| clone_path = self.cloneProject(project) |
| gpatch = self.createPatch(clone_path, project) |
| emails = self._ChooseReviewers() |
| helper = self._GetHelper() |
| helper.SetReviewers(gpatch.gerrit_number, add=(emails[0], emails[1])) |
| helper.SetAttentionSet(gpatch.gerrit_number, add=(emails[0], emails[1])) |
| attention = gob_util.GetAttentionSet(helper.host, gpatch.gerrit_number) |
| self.assertEqual(len(attention), 2) |
| self.assertCountEqual( |
| [r["account"]["email"] for r in attention], [emails[0], emails[1]] |
| ) |
| helper.SetAttentionSet(gpatch.gerrit_number, remove=(emails[0],)) |
| attention = gob_util.GetAttentionSet(helper.host, gpatch.gerrit_number) |
| self.assertEqual(len(attention), 1) |
| self.assertEqual(attention[0]["account"]["email"], emails[1]) |
| |
| def testSetReviewers(self) -> None: |
| """Verify that we can set reviewers on a CL.""" |
| project = self.createProject("testProject") |
| clone_path = self.cloneProject(project) |
| gpatch = self.createPatch(clone_path, project) |
| emails = self._ChooseReviewers() |
| helper = self._GetHelper() |
| helper.SetReviewers(gpatch.gerrit_number, add=(emails[0], emails[1])) |
| reviewers = gob_util.GetReviewers(helper.host, gpatch.gerrit_number) |
| self.assertEqual(len(reviewers), 2) |
| self.assertCountEqual( |
| [r["email"] for r in reviewers], [emails[0], emails[1]] |
| ) |
| helper.SetReviewers(gpatch.gerrit_number, remove=(emails[0],)) |
| reviewers = gob_util.GetReviewers(helper.host, gpatch.gerrit_number) |
| self.assertEqual(len(reviewers), 1) |
| self.assertEqual(reviewers[0]["email"], emails[1]) |
| |
| def testPatchNotFound(self) -> None: |
| """Test case where ChangeID isn't found on the server.""" |
| changeids = ["I" + ("deadbeef" * 5), "I" + ("beadface" * 5)] |
| self.assertRaises( |
| gerrit.GerritException, gerrit.GetGerritPatchInfo, changeids |
| ) |
| self.assertRaises( |
| gerrit.GerritException, |
| gerrit.GetGerritPatchInfo, |
| ["*" + cid for cid in changeids], |
| ) |
| # Change ID sequence starts at 1000. |
| gerrit_numbers = ["123", "543"] |
| self.assertRaises( |
| gerrit.GerritException, gerrit.GetGerritPatchInfo, gerrit_numbers |
| ) |
| self.assertRaises( |
| gerrit.GerritException, |
| gerrit.GetGerritPatchInfo, |
| ["*" + num for num in gerrit_numbers], |
| ) |
| |
| def testVagueQuery(self) -> None: |
| """Verify GerritHelper complains if an ID matches multiple changes.""" |
| project = self.createProject("testProject") |
| clone_path = self.cloneProject(project) |
| (sha1, _) = self.createCommit(clone_path) |
| (_, changeid) = self.createCommit(clone_path) |
| self.uploadChange(clone_path, "main") |
| cros_build_lib.dbg_run( |
| ["git", "checkout", sha1], cwd=clone_path, capture_output=True |
| ) |
| self.createCommit(clone_path) |
| self.pushBranch(clone_path, "testbranch") |
| self.createCommit( |
| clone_path, msg="Test commit.\n\nChange-Id: %s" % changeid |
| ) |
| self.uploadChange(clone_path, "testbranch") |
| self.assertRaises( |
| gerrit.GerritException, gerrit.GetGerritPatchInfo, [changeid] |
| ) |
| |
| def testQueries(self) -> None: |
| """Verify assorted query operations.""" |
| project = self.createProject("testProject") |
| clone_path = self.cloneProject(project) |
| gpatch = self.createPatch(clone_path, project) |
| helper = self._GetHelper() |
| |
| # Multi-queries with one valid and one invalid term should raise. |
| invalid_change_id = "I1234567890123456789012345678901234567890" |
| self.assertRaises( |
| gerrit.GerritException, |
| gerrit.GetGerritPatchInfo, |
| [invalid_change_id, gpatch.change_id], |
| ) |
| self.assertRaises( |
| gerrit.GerritException, |
| gerrit.GetGerritPatchInfo, |
| [gpatch.change_id, invalid_change_id], |
| ) |
| self.assertRaises( |
| gerrit.GerritException, |
| gerrit.GetGerritPatchInfo, |
| ["9876543", gpatch.gerrit_number], |
| ) |
| self.assertRaises( |
| gerrit.GerritException, |
| gerrit.GetGerritPatchInfo, |
| [gpatch.gerrit_number, "9876543"], |
| ) |
| |
| site_params = config_lib.GetSiteParams() |
| # Simple query by project/changeid/sha1. |
| patch_info = helper.GrabPatchFromGerrit( |
| gpatch.project, gpatch.change_id, gpatch.sha1 |
| ) |
| self.assertEqual(patch_info.gerrit_number, gpatch.gerrit_number) |
| self.assertEqual(patch_info.remote, site_params.EXTERNAL_REMOTE) |
| |
| # Simple query by gerrit number to external remote. |
| patch_info = gerrit.GetGerritPatchInfo([gpatch.gerrit_number]) |
| self.assertEqual(patch_info[0].gerrit_number, gpatch.gerrit_number) |
| self.assertEqual(patch_info[0].remote, site_params.EXTERNAL_REMOTE) |
| |
| # Simple query by gerrit number to internal remote. |
| patch_info = gerrit.GetGerritPatchInfo(["*" + gpatch.gerrit_number]) |
| self.assertEqual(patch_info[0].gerrit_number, gpatch.gerrit_number) |
| self.assertEqual(patch_info[0].remote, site_params.INTERNAL_REMOTE) |
| |
| # Query to external server by gerrit number and change-id which refer to |
| # the same change should return one result. |
| fq_changeid = "~".join((gpatch.project, "main", gpatch.change_id)) |
| patch_info = gerrit.GetGerritPatchInfo( |
| [gpatch.gerrit_number, fq_changeid] |
| ) |
| self.assertEqual(len(patch_info), 1) |
| self.assertEqual(patch_info[0].gerrit_number, gpatch.gerrit_number) |
| self.assertEqual(patch_info[0].remote, site_params.EXTERNAL_REMOTE) |
| |
| # Query to internal server by gerrit number and change-id which refer to |
| # the same change should return one result. |
| patch_info = gerrit.GetGerritPatchInfo( |
| ["*" + gpatch.gerrit_number, "*" + fq_changeid] |
| ) |
| self.assertEqual(len(patch_info), 1) |
| self.assertEqual(patch_info[0].gerrit_number, gpatch.gerrit_number) |
| self.assertEqual(patch_info[0].remote, site_params.INTERNAL_REMOTE) |
| |
| def testSubmitOutdatedCommit(self) -> None: |
| """Tests that we can parse a json to check if a change is committed.""" |
| project = self.createProject("testProject") |
| clone_path = self.cloneProject(project, "p1") |
| |
| # Create a change. |
| gpatch1 = self.createPatch(clone_path, project) |
| |
| # Update the change. |
| gpatch2 = self.createPatch(clone_path, project, amend=True) |
| |
| # Make sure we're talking about the same change. |
| self.assertEqual(gpatch1.change_id, gpatch2.change_id) |
| |
| # Try submitting the out-of-date change. |
| helper = self._GetHelper() |
| helper.SetReview(gpatch1.gerrit_number, labels={"Code-Review": "+2"}) |
| with self.assertRaises(gob_util.GOBError) as ex: |
| helper.SubmitChange(gpatch1) |
| self.assertEqual(ex.exception.http_status, http.client.CONFLICT) |
| self.assertFalse(helper.IsChangeCommitted(gpatch1.gerrit_number)) |
| |
| # Try submitting the up-to-date change. |
| helper.SubmitChange(gpatch2) |
| helper.IsChangeCommitted(gpatch2.gerrit_number) |
| |
| def testResetReviewLabels(self) -> None: |
| """Tests that we can remove a code review label.""" |
| project = self.createProject("testProject") |
| helper = self._GetHelper() |
| clone_path = self.cloneProject(project, "p1") |
| gpatch = self.createPatch(clone_path, project, msg="Init") |
| helper.SetReview(gpatch.gerrit_number, labels={"Code-Review": "+2"}) |
| gob_util.ResetReviewLabels( |
| helper.host, |
| gpatch.gerrit_number, |
| label="Code-Review", |
| notify="OWNER", |
| ) |
| |
| def testApprovalTime(self) -> None: |
| """Approval timestamp should be reset when a new patchset is created.""" |
| # Create a change. |
| project = self.createProject("testProject") |
| helper = self._GetHelper() |
| clone_path = self.cloneProject(project, "p1") |
| gpatch = self.createPatch(clone_path, project, msg="Init") |
| helper.SetReview(gpatch.gerrit_number, labels={"Code-Review": "+2"}) |
| |
| # Update the change. |
| new_msg = "New %s" % gpatch.commit_message |
| cros_build_lib.dbg_run( |
| ["git", "commit", "--amend", "-m", new_msg], |
| cwd=clone_path, |
| capture_output=True, |
| ) |
| self.uploadChange(clone_path) |
| gpatch2 = self._GetHelper().QuerySingleRecord( |
| change=gpatch.change_id, project=gpatch.project, branch="main" |
| ) |
| self.assertNotEqual(gpatch2.approval_timestamp, 0) |
| self.assertNotEqual(gpatch2.commit_timestamp, 0) |
| self.assertEqual(gpatch2.approval_timestamp, gpatch2.commit_timestamp) |
| |
| |
| class GerritParserTest(cros_test_lib.TestCase): |
| """Unittests for GerritHelper.""" |
| |
| # pylint: disable=protected-access |
| |
| def _GetHelper(self, remote=config_lib.GetSiteParams().EXTERNAL_REMOTE): |
| return gerrit.GetGerritHelper(remote) |
| |
| def testGetChangeFromStdoutPass(self) -> None: |
| """Verify the proper change number is returned from the git stdout.""" |
| stdout = ( |
| "remote:\nremote:\nremote: " |
| "https://example.com/c/some/project/repo/+/123 gerrit: test" |
| ) |
| changenum = self._GetHelper()._get_changenumber_from_stdout(stdout) |
| self.assertEqual(changenum, "123") |
| |
| stdout = ( |
| "remote:\nremote: " |
| "https://example.com/c/some/project3/repo/+/123 gerrit: test" |
| ) |
| changenum = self._GetHelper()._get_changenumber_from_stdout(stdout) |
| self.assertEqual(changenum, "123") |
| |
| stdout = ( |
| "remote: " |
| "https://example.com/c/some/project/repo/+/123 gerrit: test 456" |
| ) |
| changenum = self._GetHelper()._get_changenumber_from_stdout(stdout) |
| self.assertEqual(changenum, "123") |
| |
| stdout = ( |
| "remote: " |
| "https://example.com/c/some/project/repo/+/123 " |
| "handle /+/124 in URI" |
| ) |
| changenum = self._GetHelper()._get_changenumber_from_stdout(stdout) |
| self.assertEqual(changenum, "123") |
| |
| def testGetChangeFromStdoutFail(self) -> None: |
| """Verify the function returns None when an improper stdout is given.""" |
| |
| # Fails because remote is not at the start of the text. |
| stdout = """ |
| remote: |
| remote: https://example./c/some/project/repo/+/123 gerrit: test |
| """ |
| changenum = self._GetHelper()._get_changenumber_from_stdout(stdout) |
| self.assertIsNone(changenum) |
| |
| stdout = """ |
| remote:https://example./c/some/project/repo/+/123 gerrit: test |
| """ |
| changenum = self._GetHelper()._get_changenumber_from_stdout(stdout) |
| |
| self.assertIsNone(changenum) |
| |
| |
| @cros_test_lib.pytestmark_network_test |
| class DirectGerritHelperTest(cros_test_lib.TestCase): |
| """Unittests for GerritHelper that use the real Chromium instance.""" |
| |
| # A big list of real changes. |
| CHANGES = ["235893", "*189165", "231790", "*190026", "231647", "234645"] |
| |
| def testMultipleChangeDetail(self) -> None: |
| """Test ordering of results in GetMultipleChangeDetail""" |
| changes = [x for x in self.CHANGES if not x.startswith("*")] |
| helper = gerrit.GetCrosExternal() |
| results = list( |
| helper.GetMultipleChangeDetail([str(x) for x in changes]) |
| ) |
| gerrit_numbers = [str(x["_number"]) for x in results] |
| self.assertEqual(changes, gerrit_numbers) |
| |
| def testQueryMultipleCurrentPatchset(self) -> None: |
| """Test ordering of results in QueryMultipleCurrentPatchset""" |
| changes = [x for x in self.CHANGES if not x.startswith("*")] |
| helper = gerrit.GetCrosExternal() |
| results = list(helper.QueryMultipleCurrentPatchset(changes)) |
| self.assertEqual(changes, [x.gerrit_number for _, x in results]) |
| self.assertEqual(changes, [x for x, _ in results]) |
| |
| def testGetGerritPatchInfo(self) -> None: |
| """Test ordering of results in GetGerritPatchInfo""" |
| # Swizzle from our old syntax to the new syntax. |
| changes = [] |
| for change in self.CHANGES: |
| if change.startswith("*"): |
| changes.append("chrome-internal:%s" % (change[1:],)) |
| else: |
| changes.append("chromium:%s" % (change,)) |
| results = list(gerrit.GetGerritPatchInfo(changes)) |
| self.assertEqual(changes, [x.gerrit_number_str for x in results]) |