Give better error messages when changes aren't mergeable.
Instead of reporting that a change is "not commit ready
anymore", we should give a specific error message explaining what the
exact problem is.
BUG=chromium:336632
TEST=Updated unit tests.
Change-Id: Ibd852afab6d2603dd9a068b99145d88d1af92068
Reviewed-on: https://chromium-review.googlesource.com/235790
Reviewed-by: Don Garrett <dgarrett@chromium.org>
Tested-by: David James <davidjames@chromium.org>
Trybot-Ready: David James <davidjames@chromium.org>
Reviewed-by: Aviv Keshet <akeshet@chromium.org>
Commit-Queue: David James <davidjames@chromium.org>
diff --git a/cbuildbot/validation_pool.py b/cbuildbot/validation_pool.py
index 17dbae8..5949941 100644
--- a/cbuildbot/validation_pool.py
+++ b/cbuildbot/validation_pool.py
@@ -114,13 +114,6 @@
"""Raised if we try to apply a not-in-manifest change."""
-class PatchNotCommitReady(cros_patch.PatchException):
- """Raised if a patch is not marked as commit ready."""
-
- def ShortExplanation(self):
- return 'isn\'t marked as Commit-Ready anymore.'
-
-
class PatchModified(cros_patch.PatchException):
"""Raised if a patch is modified while the CQ is running."""
@@ -483,7 +476,7 @@
if self._is_submitting:
raise PatchRejected(dep_change)
else:
- raise PatchNotCommitReady(dep_change)
+ raise dep_change.GetMergeException() or PatchRejected(dep_change)
unsatisfied.append(dep_change)
@@ -1670,13 +1663,7 @@
raise TreeIsClosedException(closed_or_throttled=not throttled_ok)
# Filter out changes that were modified during the CQ run.
- unmodified_changes, errors = self.FilterModifiedChanges(changes)
-
- # Filter out changes that aren't marked as CR=+2, CQ=+1, V=+1 anymore, in
- # case the patch status changed during the CQ run.
- filtered_changes = [x for x in unmodified_changes if x.IsMergeable()]
- for change in set(unmodified_changes) - set(filtered_changes):
- errors[change] = PatchNotCommitReady(change)
+ filtered_changes, errors = self.FilterModifiedChanges(changes)
patch_series = PatchSeries(self.build_root, helper_pool=self._helper_pool,
is_submitting=True)
@@ -1739,8 +1726,8 @@
Returns:
This returns a tuple (unmodified_changes, errors).
- unmodified_changes: A reloaded list of changes, only including unmodified
- and unsubmitted changes.
+ unmodified_changes: A reloaded list of changes, only including mergeable,
+ unmodified and unsubmitted changes.
errors: A dictionary. This dictionary will contain all patches that have
encountered errors, and map them to the associated exception object.
"""
@@ -1749,15 +1736,18 @@
# that occurs below will be mostly up-to-date.
unmodified_changes, errors = [], {}
reloaded_changes = list(cls.ReloadChanges(changes))
- old_changes = cros_patch.PatchCache(changes)
- for change in reloaded_changes:
- if change.IsAlreadyMerged():
+ for change, reloaded_change in zip(changes, reloaded_changes):
+ if reloaded_change.IsAlreadyMerged():
logging.warning('%s is already merged. It was most likely chumped '
'during the current CQ run.', change)
- elif change.patch_number != old_changes[change].patch_number:
+ elif reloaded_change.patch_number != change.patch_number:
# If users upload new versions of a CL while the CQ is in-flight, then
# their CLs are no longer tested. These CLs should be rejected.
errors[change] = PatchModified(change)
+ elif not reloaded_change.IsMergeable():
+ # Get the reason why this change is not mergeable anymore.
+ errors[change] = reloaded_change.GetMergeException()
+ errors[change].patch = change
else:
unmodified_changes.append(change)
diff --git a/cbuildbot/validation_pool_unittest.py b/cbuildbot/validation_pool_unittest.py
index 4e8fbc1..87848b0 100755
--- a/cbuildbot/validation_pool_unittest.py
+++ b/cbuildbot/validation_pool_unittest.py
@@ -1450,13 +1450,17 @@
"""Test that a CL is rejected if its approvals were pulled."""
def _ReloadPatches(patches):
reloaded = copy.deepcopy(patches)
- self.PatchObject(reloaded[1], 'HasApproval', return_value=False)
+ approvals = {('VRIF', '1'): False}
+ backup = reloaded[1].HasApproval
+ self.PatchObject(
+ reloaded[1], 'HasApproval',
+ side_effect=lambda *args: approvals.get(args, backup(*args)))
return reloaded
self.PatchObject(gerrit, 'GetGerritPatchInfoWithPatchQueries',
_ReloadPatches)
self.SubmitPool(submitted=self.patches[:1], rejected=self.patches[1:])
- error = validation_pool.PatchNotCommitReady(self.patches[1])
- self.assertEqualNotifyArg(error, self.patches[1], 'error')
+ message = 'CL:2 is not marked Verified=+1.'
+ self.assertEqualNotifyArg(message, self.patches[1], 'error')
def testAlreadyMerged(self):
"""Test that a CL that was chumped during the run was not rejected."""
diff --git a/lib/patch.py b/lib/patch.py
index bead1bd..fb2ae7c 100644
--- a/lib/patch.py
+++ b/lib/patch.py
@@ -294,6 +294,18 @@
return 'could not be found in the repo manifest.'
+class PatchNotMergeable(PatchException):
+ """Raised if a patch is not mergeable."""
+
+ def __init__(self, patch, reason):
+ PatchException.__init__(self, patch)
+ self.reason = str(reason)
+ self.args = (patch, reason)
+
+ def ShortExplanation(self):
+ return self.reason
+
+
def MakeChangeId(unusable=False):
"""Create a random Change-Id.
@@ -1534,14 +1546,40 @@
def IsMergeable(self):
"""Return true if all Gerrit approvals required for submission are set."""
- return (self.status == 'NEW' and not self.WasVetoed() and
- not self.IsDraft() and
- self.HasApprovals({'CRVW': '2', 'VRIF': '1', 'COMR': ('1', '2')}))
+ return not self.GetMergeException()
def HasReadyFlag(self):
"""Return true if the trybot-ready or commit-ready flag is set."""
return self.HasApproval('COMR', ('1', '2')) or self.HasApproval('TRY', '1')
+ def GetMergeException(self):
+ """Return the reason why this change is not mergeable.
+
+ If the change is in fact mergeable, return None.
+ """
+ if self.IsDraft():
+ return PatchNotMergeable(self, 'is a draft.')
+
+ if self.status != 'NEW':
+ statuses = {
+ 'MERGED': 'is already merged.',
+ 'SUBMITTED': 'is being merged.',
+ 'ABANDONED': 'is abandoned.',
+ }
+ message = statuses.get(self.status, 'has status %s.' % self.status)
+ return PatchNotMergeable(self, message)
+
+ if self.HasApproval('VRIF', '-1'):
+ return PatchNotMergeable(self, 'is marked as Verified=-1.')
+ elif self.HasApproval('CRVW', '-2'):
+ return PatchNotMergeable(self, 'is marked as Code-Review=-2.')
+ elif not self.HasApproval('CRVW', '2'):
+ return PatchNotMergeable(self, 'is not marked Code-Review=+2.')
+ elif not self.HasApproval('VRIF', '1'):
+ return PatchNotMergeable(self, 'is not marked Verified=+1.')
+ elif not self.HasApproval('COMR', ('1', '2')):
+ return PatchNotMergeable(self, 'is not marked Commit-Queue>=+1.')
+
def GetLatestApproval(self, field):
"""Return most recent value of specific field on the current patchset.