summaryrefslogtreecommitdiffstats
path: root/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java')
-rw-r--r--javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java1976
1 files changed, 1976 insertions, 0 deletions
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java b/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java
new file mode 100644
index 0000000000..fc2a272082
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/ChangeBundleTest.java
@@ -0,0 +1,1976 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
+import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+import static com.google.gerrit.server.util.time.TimeUtil.truncateToSecond;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Table;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.testing.TestChanges;
+import com.google.gerrit.testing.TestTimeUtil;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.Month;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.TimeZone;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ChangeBundleTest extends GerritBaseTests {
+ private static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class);
+ private static final ProtobufCodec<ChangeMessage> CHANGE_MESSAGE_CODEC =
+ CodecFactory.encoder(ChangeMessage.class);
+ private static final ProtobufCodec<PatchSet> PATCH_SET_CODEC =
+ CodecFactory.encoder(PatchSet.class);
+ private static final ProtobufCodec<PatchSetApproval> PATCH_SET_APPROVAL_CODEC =
+ CodecFactory.encoder(PatchSetApproval.class);
+ private static final ProtobufCodec<PatchLineComment> PATCH_LINE_COMMENT_CODEC =
+ CodecFactory.encoder(PatchLineComment.class);
+ private static final String TIMEZONE_ID = "US/Eastern";
+
+ private String systemTimeZoneProperty;
+ private TimeZone systemTimeZone;
+
+ private Project.NameKey project;
+ private Account.Id accountId;
+
+ @Before
+ public void setUp() {
+ systemTimeZoneProperty = System.setProperty("user.timezone", TIMEZONE_ID);
+ systemTimeZone = TimeZone.getDefault();
+ TimeZone.setDefault(TimeZone.getTimeZone(TIMEZONE_ID));
+ long maxMs = ChangeRebuilderImpl.MAX_WINDOW_MS;
+ assertThat(maxMs).isGreaterThan(1000L);
+ TestTimeUtil.resetWithClockStep(maxMs * 2, MILLISECONDS);
+ project = new Project.NameKey("project");
+ accountId = new Account.Id(100);
+ }
+
+ @After
+ public void tearDown() {
+ TestTimeUtil.useSystemTime();
+ System.setProperty("user.timezone", systemTimeZoneProperty);
+ TimeZone.setDefault(systemTimeZone);
+ }
+
+ private void superWindowResolution() {
+ TestTimeUtil.setClockStep(ChangeRebuilderImpl.MAX_WINDOW_MS * 2, MILLISECONDS);
+ TimeUtil.nowTs();
+ }
+
+ private void subWindowResolution() {
+ TestTimeUtil.setClockStep(1, SECONDS);
+ TimeUtil.nowTs();
+ }
+
+ @Test
+ public void diffChangesDifferentIds() throws Exception {
+ Change c1 = TestChanges.newChange(project, accountId);
+ int id1 = c1.getId().get();
+ Change c2 = TestChanges.newChange(project, accountId);
+ int id2 = c2.getId().get();
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+
+ assertDiffs(
+ b1,
+ b2,
+ "changeId differs for Changes: {" + id1 + "} != {" + id2 + "}",
+ "createdOn differs for Changes: {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}",
+ "effective last updated time differs for Changes:"
+ + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}");
+ }
+
+ @Test
+ public void diffChangesSameId() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ Change c2 = clone(c1);
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+
+ assertNoDiffs(b1, b2);
+
+ c2.setTopic("topic");
+ assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {null} != {topic}");
+ }
+
+ @Test
+ public void diffChangesMixedSourcesAllowsSlop() throws Exception {
+ subWindowResolution();
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ Change c2 = clone(c1);
+ c2.setCreatedOn(TimeUtil.nowTs());
+ c2.setLastUpdatedOn(TimeUtil.nowTs());
+
+ // Both are ReviewDb, exact timestamp match is required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "createdOn differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:02.0}",
+ "effective last updated time differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:03.0}");
+
+ // One NoteDb, slop is allowed.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ // But not too much slop.
+ superWindowResolution();
+ Change c3 = clone(c1);
+ c3.setLastUpdatedOn(TimeUtil.nowTs());
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ ChangeBundle b3 =
+ new ChangeBundle(
+ c3, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ String msg =
+ "effective last updated time differs for Change.Id "
+ + c1.getId()
+ + " in NoteDb vs. ReviewDb:"
+ + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:10.0}";
+ assertDiffs(b1, b3, msg);
+ assertDiffs(b3, b1, msg);
+ }
+
+ @Test
+ public void diffChangesIgnoresOriginalSubjectInReviewDb() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ c1.setCurrentPatchSet(c1.currentPatchSetId(), "Subject", "Original A");
+ Change c2 = clone(c1);
+ c2.setCurrentPatchSet(c2.currentPatchSetId(), c1.getSubject(), "Original B");
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "originalSubject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Original A} != {Original B}");
+
+ // Both NoteDb, exact match required.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "originalSubject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Original A} != {Original B}");
+
+ // One ReviewDb, one NoteDb, original subject is ignored.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
+ public void diffChangesSanitizesSubjectsBeforeComparison() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ c1.setCurrentPatchSet(c1.currentPatchSetId(), "Subject\r\rbody", "Original");
+ Change c2 = clone(c1);
+ c2.setCurrentPatchSet(c2.currentPatchSetId(), "Subject body", "Original");
+
+ // Both ReviewDb, exact match required
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Subject\r\rbody} != {Subject body}");
+
+ // Both NoteDb, exact match required (although it should be impossible to
+ // create a NoteDb change with '\r' in the subject).
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Subject\r\rbody} != {Subject body}");
+
+ // One ReviewDb, one NoteDb, '\r' is normalized to ' '.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
+ public void diffChangesConsidersEmptyReviewDbTopicEquivalentToNullInNoteDb() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ c1.setTopic("");
+ Change c2 = clone(c1);
+ c2.setTopic(null);
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {} != {null}");
+
+ // Topic ignored if ReviewDb is empty and NoteDb is null.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+
+ // Exact match still required if NoteDb has empty value (not realistic).
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {} != {null}");
+
+ // Null is not equal to a non-empty string.
+ Change c3 = clone(c1);
+ c3.setTopic("topic");
+ b1 =
+ new ChangeBundle(
+ c3, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {topic} != {null}");
+
+ // Null is equal to a string that is all whitespace.
+ Change c4 = clone(c1);
+ c4.setTopic(" ");
+ b1 =
+ new ChangeBundle(
+ c4, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
+ public void diffChangesIgnoresLeadingAndTrailingWhitespaceInReviewDbTopics() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ c1.setTopic(" abc ");
+ Change c2 = clone(c1);
+ c2.setTopic("abc");
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": { abc } != {abc}");
+
+ // Leading whitespace in ReviewDb topic is ignored.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ // Must match except for the leading/trailing whitespace.
+ Change c3 = clone(c1);
+ c3.setTopic("cba");
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c3, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": { abc } != {cba}");
+ }
+
+ @Test
+ public void diffChangesTakesMaxEntityTimestampFromReviewDb() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ PatchSet ps = new PatchSet(c1.currentPatchSetId());
+ ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ ps.setUploader(accountId);
+ ps.setCreatedOn(TimeUtil.nowTs());
+ PatchSetApproval a =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(c1.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+ (short) 1,
+ TimeUtil.nowTs());
+
+ Change c2 = clone(c1);
+ c2.setLastUpdatedOn(a.getGranted());
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(ps), approvals(a), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(ps), approvals(a), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "effective last updated time differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:12.0}");
+
+ // NoteDb allows latest timestamp from all entities in bundle.
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(ps), approvals(a), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ }
+
+ @Test
+ public void diffChangesIgnoresChangeTimestampIfAnyOtherEntitiesExist() {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ PatchSet ps = new PatchSet(c1.currentPatchSetId());
+ ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ ps.setUploader(accountId);
+ ps.setCreatedOn(TimeUtil.nowTs());
+ PatchSetApproval a =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(c1.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+ (short) 1,
+ TimeUtil.nowTs());
+ c1.setLastUpdatedOn(a.getGranted());
+
+ Change c2 = clone(c1);
+ c2.setLastUpdatedOn(TimeUtil.nowTs());
+
+ // ReviewDb has later lastUpdatedOn timestamp than NoteDb, allowed since
+ // NoteDb matches the latest timestamp of a non-Change entity.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c2, messages(), patchSets(ps), approvals(a), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c1, messages(), patchSets(ps), approvals(a), comments(), reviewers(), NOTE_DB);
+ assertThat(b1.getChange().getLastUpdatedOn()).isGreaterThan(b2.getChange().getLastUpdatedOn());
+ assertNoDiffs(b1, b2);
+
+ // Timestamps must actually match if Change is the only entity.
+ b1 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "effective last updated time differs for Change.Id "
+ + c1.getId()
+ + " in NoteDb vs. ReviewDb:"
+ + " {2009-09-30 17:00:12.0} != {2009-09-30 17:00:18.0}");
+ }
+
+ @Test
+ public void diffChangesAllowsReviewDbSubjectToBePrefixOfNoteDbSubject() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ Change c2 = clone(c1);
+ c2.setCurrentPatchSet(
+ c1.currentPatchSetId(), c1.getSubject().substring(0, 10), c1.getOriginalSubject());
+ assertThat(c2.getSubject()).isNotEqualTo(c1.getSubject());
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "subject differs for Change.Id " + c1.getId() + ": {Change subject} != {Change sub}");
+
+ // ReviewDb has shorter subject, allowed.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+
+ // NoteDb has shorter subject, not allowed.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), latest(c1), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(c2, messages(), latest(c2), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "subject differs for Change.Id " + c1.getId() + ": {Change subject} != {Change sub}");
+ }
+
+ @Test
+ public void diffChangesTrimsLeadingSpacesFromReviewDbComparingToNoteDb() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ Change c2 = clone(c1);
+ c2.setCurrentPatchSet(c1.currentPatchSetId(), " " + c1.getSubject(), c1.getOriginalSubject());
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Change subject} != { Change subject}");
+
+ // ReviewDb is missing leading spaces, allowed.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
+ public void diffChangesDoesntTrimLeadingNonSpaceWhitespaceFromSubject() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ Change c2 = clone(c1);
+ c2.setCurrentPatchSet(c1.currentPatchSetId(), "\t" + c1.getSubject(), c1.getOriginalSubject());
+
+ // Both ReviewDb.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Change subject} != {\tChange subject}");
+
+ // One NoteDb.
+ b1 =
+ new ChangeBundle(c1, messages(), latest(c1), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), latest(c2), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Change subject} != {\tChange subject}");
+ assertDiffs(
+ b2,
+ b1,
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {\tChange subject} != {Change subject}");
+ }
+
+ @Test
+ public void diffChangesHandlesBuggyJGitSubjectExtraction() throws Exception {
+ Change c1 = TestChanges.newChange(project, accountId);
+ String buggySubject = "Subject\r \r Rest of message.";
+ c1.setCurrentPatchSet(c1.currentPatchSetId(), buggySubject, buggySubject);
+ Change c2 = clone(c1);
+ c2.setCurrentPatchSet(c2.currentPatchSetId(), "Subject", "Subject");
+
+ // Both ReviewDb.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "originalSubject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Subject\r \r Rest of message.} != {Subject}",
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Subject\r \r Rest of message.} != {Subject}");
+
+ // NoteDb has correct subject without "\r ".
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
+ public void diffChangesIgnoresInvalidCurrentPatchSetIdInReviewDb() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ Change c2 = clone(c1);
+ c2.setCurrentPatchSet(
+ new PatchSet.Id(c2.getId(), 0), "Unrelated subject", c2.getOriginalSubject());
+
+ // Both ReviewDb.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "currentPatchSetId differs for Change.Id " + c1.getId() + ": {1} != {0}",
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Change subject} != {Unrelated subject}");
+
+ // One NoteDb.
+ //
+ // This is based on a real corrupt change where all patch sets were deleted
+ // but the Change entity stuck around, resulting in a currentPatchSetId of 0
+ // after converting to NoteDb.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
+ public void diffChangesAllowsCreatedToMatchLastUpdated() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ c1.setCreatedOn(TimeUtil.nowTs());
+ assertThat(c1.getCreatedOn()).isGreaterThan(c1.getLastUpdatedOn());
+ Change c2 = clone(c1);
+ c2.setCreatedOn(c2.getLastUpdatedOn());
+
+ // Both ReviewDb.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "createdOn differs for Change.Id "
+ + c1.getId()
+ + ": {2009-09-30 17:00:06.0} != {2009-09-30 17:00:00.0}");
+
+ // One NoteDb.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
+ public void diffChangeMessageKeySets() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ int id = c.getId().get();
+ ChangeMessage cm1 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid1"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ ChangeMessage cm2 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid2"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+
+ assertDiffs(
+ b1,
+ b2,
+ "ChangeMessage.Key sets differ:"
+ + " ["
+ + id
+ + ",uuid1] only in A; ["
+ + id
+ + ",uuid2] only in B");
+ }
+
+ @Test
+ public void diffChangeMessages() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ ChangeMessage cm1 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ cm1.setMessage("message 1");
+ ChangeMessage cm2 = clone(cm1);
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+
+ assertNoDiffs(b1, b2);
+
+ cm2.setMessage("message 2");
+ assertDiffs(
+ b1,
+ b2,
+ "message differs for ChangeMessage.Key "
+ + c.getId()
+ + ",uuid:"
+ + " {message 1} != {message 2}");
+ }
+
+ @Test
+ public void diffChangeMessagesIgnoresUuids() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ int id = c.getId().get();
+ ChangeMessage cm1 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid1"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ cm1.setMessage("message 1");
+ ChangeMessage cm2 = clone(cm1);
+ cm2.getKey().set("uuid2");
+
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ // Both are ReviewDb, exact UUID match is required.
+ assertDiffs(
+ b1,
+ b2,
+ "ChangeMessage.Key sets differ:"
+ + " ["
+ + id
+ + ",uuid1] only in A; ["
+ + id
+ + ",uuid2] only in B");
+
+ // One NoteDb, UUIDs are ignored.
+ b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(cm2), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ }
+
+ @Test
+ public void diffChangeMessagesWithDifferentCounts() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ int id = c.getId().get();
+ ChangeMessage cm1 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid1"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ cm1.setMessage("message 1");
+ ChangeMessage cm2 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid2"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ cm1.setMessage("message 2");
+
+ // Both ReviewDb: Uses same keySet diff as other types.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(cm1, cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1, b2, "ChangeMessage.Key sets differ: [" + id + ",uuid2] only in A; [] only in B");
+
+ // One NoteDb: UUIDs in keys can't be used for comparison, just diff counts.
+ b1 =
+ new ChangeBundle(
+ c, messages(cm1, cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(b1, b2, "ChangeMessages differ for Change.Id " + id + "\nOnly in A:\n " + cm2);
+ assertDiffs(b2, b1, "ChangeMessages differ for Change.Id " + id + "\nOnly in B:\n " + cm2);
+ }
+
+ @Test
+ public void diffChangeMessagesMixedSourcesWithDifferences() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ int id = c.getId().get();
+ ChangeMessage cm1 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid1"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ cm1.setMessage("message 1");
+ ChangeMessage cm2 = clone(cm1);
+ cm2.setMessage("message 2");
+ ChangeMessage cm3 = clone(cm1);
+ cm3.getKey().set("uuid2"); // Differs only in UUID.
+
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(cm1, cm3), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(cm2, cm3), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
+ // Implementation happens to pair up cm1 in b1 with cm3 in b2 because it
+ // depends on iteration order and doesn't care about UUIDs. The important
+ // thing is that there's some diff.
+ assertDiffs(
+ b1,
+ b2,
+ "ChangeMessages differ for Change.Id "
+ + id
+ + "\n"
+ + "Only in A:\n "
+ + cm3
+ + "\n"
+ + "Only in B:\n "
+ + cm2);
+ assertDiffs(
+ b2,
+ b1,
+ "ChangeMessages differ for Change.Id "
+ + id
+ + "\n"
+ + "Only in A:\n "
+ + cm2
+ + "\n"
+ + "Only in B:\n "
+ + cm3);
+ }
+
+ @Test
+ public void diffChangeMessagesMixedSourcesAllowsSlop() throws Exception {
+ subWindowResolution();
+ Change c = TestChanges.newChange(project, accountId);
+ ChangeMessage cm1 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid1"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ ChangeMessage cm2 = clone(cm1);
+ cm2.setWrittenOn(TimeUtil.nowTs());
+
+ // Both are ReviewDb, exact timestamp match is required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "writtenOn differs for ChangeMessage.Key "
+ + c.getId()
+ + ",uuid1:"
+ + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
+
+ // One NoteDb, slop is allowed.
+ b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ // But not too much slop.
+ superWindowResolution();
+ ChangeMessage cm3 = clone(cm1);
+ cm3.setWrittenOn(TimeUtil.nowTs());
+ b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
+ ChangeBundle b3 =
+ new ChangeBundle(
+ c, messages(cm3), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ int id = c.getId().get();
+ assertDiffs(
+ b1,
+ b3,
+ "ChangeMessages differ for Change.Id "
+ + id
+ + "\n"
+ + "Only in A:\n "
+ + cm1
+ + "\n"
+ + "Only in B:\n "
+ + cm3);
+ assertDiffs(
+ b3,
+ b1,
+ "ChangeMessages differ for Change.Id "
+ + id
+ + "\n"
+ + "Only in A:\n "
+ + cm3
+ + "\n"
+ + "Only in B:\n "
+ + cm1);
+ }
+
+ @Test
+ public void diffChangeMessagesAllowsNullPatchSetIdFromReviewDb() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ int id = c.getId().get();
+ ChangeMessage cm1 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ cm1.setMessage("message 1");
+ ChangeMessage cm2 = clone(cm1);
+ cm2.setPatchSetId(null);
+
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+
+ // Both are ReviewDb, exact patch set ID match is required.
+ assertDiffs(
+ b1,
+ b2,
+ "patchset differs for ChangeMessage.Key "
+ + c.getId()
+ + ",uuid:"
+ + " {"
+ + id
+ + ",1} != {null}");
+
+ // Null patch set ID on ReviewDb is ignored.
+ b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(cm2), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+
+ // Null patch set ID on NoteDb is not ignored (but is not realistic).
+ b1 =
+ new ChangeBundle(
+ c, messages(cm1), latest(c), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(cm2), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "ChangeMessages differ for Change.Id "
+ + id
+ + "\n"
+ + "Only in A:\n "
+ + cm1
+ + "\n"
+ + "Only in B:\n "
+ + cm2);
+ assertDiffs(
+ b2,
+ b1,
+ "ChangeMessages differ for Change.Id "
+ + id
+ + "\n"
+ + "Only in A:\n "
+ + cm2
+ + "\n"
+ + "Only in B:\n "
+ + cm1);
+ }
+
+ @Test
+ public void diffPatchSetIdSets() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ TestChanges.incrementPatchSet(c);
+
+ PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
+ ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ ps1.setUploader(accountId);
+ ps1.setCreatedOn(TimeUtil.nowTs());
+ PatchSet ps2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
+ ps2.setRevision(new RevId("badc0feebadc0feebadc0feebadc0feebadc0fee"));
+ ps2.setUploader(accountId);
+ ps2.setCreatedOn(TimeUtil.nowTs());
+
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1, ps2), approvals(), comments(), reviewers(), REVIEW_DB);
+
+ assertDiffs(b1, b2, "PatchSet.Id sets differ: [] only in A; [" + c.getId() + ",1] only in B");
+ }
+
+ @Test
+ public void diffPatchSets() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ PatchSet ps1 = new PatchSet(c.currentPatchSetId());
+ ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ ps1.setUploader(accountId);
+ ps1.setCreatedOn(TimeUtil.nowTs());
+ PatchSet ps2 = clone(ps1);
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
+
+ assertNoDiffs(b1, b2);
+
+ ps2.setRevision(new RevId("badc0feebadc0feebadc0feebadc0feebadc0fee"));
+ assertDiffs(
+ b1,
+ b2,
+ "revision differs for PatchSet.Id "
+ + c.getId()
+ + ",1:"
+ + " {RevId{deadbeefdeadbeefdeadbeefdeadbeefdeadbeef}}"
+ + " != {RevId{badc0feebadc0feebadc0feebadc0feebadc0fee}}");
+ }
+
+ @Test
+ public void diffPatchSetsMixedSourcesAllowsSlop() throws Exception {
+ subWindowResolution();
+ Change c = TestChanges.newChange(project, accountId);
+ PatchSet ps1 = new PatchSet(c.currentPatchSetId());
+ ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ ps1.setUploader(accountId);
+ ps1.setCreatedOn(truncateToSecond(TimeUtil.nowTs()));
+ PatchSet ps2 = clone(ps1);
+ ps2.setCreatedOn(TimeUtil.nowTs());
+
+ // Both are ReviewDb, exact timestamp match is required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "createdOn differs for PatchSet.Id "
+ + c.getId()
+ + ",1:"
+ + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
+
+ // One NoteDb, slop is allowed.
+ b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+
+ // But not too much slop.
+ superWindowResolution();
+ PatchSet ps3 = clone(ps1);
+ ps3.setCreatedOn(TimeUtil.nowTs());
+ b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), NOTE_DB);
+ ChangeBundle b3 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps3), approvals(), comments(), reviewers(), REVIEW_DB);
+ String msg =
+ "createdOn differs for PatchSet.Id "
+ + c.getId()
+ + ",1 in NoteDb vs. ReviewDb:"
+ + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:10.0}";
+ assertDiffs(b1, b3, msg);
+ assertDiffs(b3, b1, msg);
+ }
+
+ @Test
+ public void diffPatchSetsIgnoresTrailingNewlinesInPushCertificate() throws Exception {
+ subWindowResolution();
+ Change c = TestChanges.newChange(project, accountId);
+ PatchSet ps1 = new PatchSet(c.currentPatchSetId());
+ ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ ps1.setUploader(accountId);
+ ps1.setCreatedOn(truncateToSecond(TimeUtil.nowTs()));
+ ps1.setPushCertificate("some cert");
+ PatchSet ps2 = clone(ps1);
+ ps2.setPushCertificate(ps2.getPushCertificate() + "\n\n");
+
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), NOTE_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
+ public void diffPatchSetsGreaterThanCurrent() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+
+ PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
+ ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ ps1.setUploader(accountId);
+ ps1.setCreatedOn(TimeUtil.nowTs());
+ PatchSet ps2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
+ ps2.setRevision(new RevId("badc0feebadc0feebadc0feebadc0feebadc0fee"));
+ ps2.setUploader(accountId);
+ ps2.setCreatedOn(TimeUtil.nowTs());
+ assertThat(ps2.getId().get()).isGreaterThan(c.currentPatchSetId().get());
+
+ ChangeMessage cm1 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid1"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+ ChangeMessage cm2 =
+ new ChangeMessage(
+ new ChangeMessage.Key(c.getId(), "uuid2"),
+ accountId,
+ TimeUtil.nowTs(),
+ c.currentPatchSetId());
+
+ PatchSetApproval a1 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(ps1.getId(), accountId, new LabelId("Code-Review")),
+ (short) 1,
+ TimeUtil.nowTs());
+ PatchSetApproval a2 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(ps2.getId(), accountId, new LabelId("Code-Review")),
+ (short) 1,
+ TimeUtil.nowTs());
+
+ // Both ReviewDb.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(cm1), patchSets(ps1), approvals(a1), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c,
+ messages(cm1, cm2),
+ patchSets(ps1, ps2),
+ approvals(a1, a2),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "ChangeMessage.Key sets differ: [] only in A; [" + cm2.getKey() + "] only in B",
+ "PatchSet.Id sets differ: [] only in A; [" + ps2.getId() + "] only in B",
+ "PatchSetApproval.Key sets differ: [] only in A; [" + a2.getKey() + "] only in B");
+
+ // One NoteDb.
+ b1 =
+ new ChangeBundle(
+ c, messages(cm1), patchSets(ps1), approvals(a1), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c,
+ messages(cm1, cm2),
+ patchSets(ps1, ps2),
+ approvals(a1, a2),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "ChangeMessages differ for Change.Id " + c.getId() + "\nOnly in B:\n " + cm2,
+ "PatchSet.Id sets differ: [] only in A; [" + ps2.getId() + "] only in B",
+ "PatchSetApproval.Key sets differ: [] only in A; [" + a2.getKey() + "] only in B");
+
+ // Both NoteDb.
+ b1 =
+ new ChangeBundle(
+ c, messages(cm1), patchSets(ps1), approvals(a1), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c,
+ messages(cm1, cm2),
+ patchSets(ps1, ps2),
+ approvals(a1, a2),
+ comments(),
+ reviewers(),
+ NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "ChangeMessages differ for Change.Id " + c.getId() + "\nOnly in B:\n " + cm2,
+ "PatchSet.Id sets differ: [] only in A; [" + ps2.getId() + "] only in B",
+ "PatchSetApproval.Key sets differ: [] only in A; [" + a2.getKey() + "] only in B");
+ }
+
+ @Test
+ public void diffPatchSetsIgnoresLeadingAndTrailingWhitespaceInReviewDbDescriptions()
+ throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+
+ PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
+ ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ ps1.setUploader(accountId);
+ ps1.setCreatedOn(TimeUtil.nowTs());
+ ps1.setDescription(" abc ");
+ PatchSet ps2 = clone(ps1);
+ ps2.setDescription("abc");
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ": { abc } != {abc}");
+
+ // Whitespace in ReviewDb description is ignored.
+ b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ // Must match except for the leading/trailing whitespace.
+ PatchSet ps3 = clone(ps1);
+ ps3.setDescription("cba");
+ b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps3), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(
+ b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ": { abc } != {cba}");
+ }
+
+ @Test
+ public void diffPatchSetsIgnoresCreatedOnWhenReviewDbIsNonMonotonic() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+
+ Timestamp beforePs1 = TimeUtil.nowTs();
+
+ PatchSet goodPs1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
+ goodPs1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ goodPs1.setUploader(accountId);
+ goodPs1.setCreatedOn(TimeUtil.nowTs());
+ PatchSet goodPs2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
+ goodPs2.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ goodPs2.setUploader(accountId);
+ goodPs2.setCreatedOn(TimeUtil.nowTs());
+ assertThat(goodPs2.getCreatedOn()).isGreaterThan(goodPs1.getCreatedOn());
+
+ PatchSet badPs2 = clone(goodPs2);
+ badPs2.setCreatedOn(beforePs1);
+ assertThat(badPs2.getCreatedOn()).isLessThan(goodPs1.getCreatedOn());
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, goodPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, badPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "createdOn differs for PatchSet.Id "
+ + badPs2.getId()
+ + ":"
+ + " {2009-09-30 17:00:18.0} != {2009-09-30 17:00:06.0}");
+
+ // Non-monotonic in ReviewDb but monotonic in NoteDb, timestamps are
+ // ignored, including for ps1.
+ PatchSet badPs1 = clone(goodPs1);
+ badPs1.setCreatedOn(TimeUtil.nowTs());
+ b1 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(badPs1, badPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, goodPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ // Non-monotonic in NoteDb but monotonic in ReviewDb, timestamps are not
+ // ignored.
+ b1 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, goodPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(badPs1, badPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "createdOn differs for PatchSet.Id "
+ + badPs1.getId()
+ + " in NoteDb vs. ReviewDb:"
+ + " {2009-09-30 17:00:24.0} != {2009-09-30 17:00:12.0}",
+ "createdOn differs for PatchSet.Id "
+ + badPs2.getId()
+ + " in NoteDb vs. ReviewDb:"
+ + " {2009-09-30 17:00:06.0} != {2009-09-30 17:00:18.0}");
+ }
+
+ @Test
+ public void diffPatchSetsAllowsFirstPatchSetCreatedOnToMatchChangeCreatedOn() {
+ Change c = TestChanges.newChange(project, accountId);
+ c.setLastUpdatedOn(TimeUtil.nowTs());
+
+ PatchSet goodPs1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
+ goodPs1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ goodPs1.setUploader(accountId);
+ goodPs1.setCreatedOn(TimeUtil.nowTs());
+ assertThat(goodPs1.getCreatedOn()).isGreaterThan(c.getCreatedOn());
+
+ PatchSet ps1AtCreatedOn = clone(goodPs1);
+ ps1AtCreatedOn.setCreatedOn(c.getCreatedOn());
+
+ PatchSet goodPs2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
+ goodPs2.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ goodPs2.setUploader(accountId);
+ goodPs2.setCreatedOn(TimeUtil.nowTs());
+
+ PatchSet ps2AtCreatedOn = clone(goodPs2);
+ ps2AtCreatedOn.setCreatedOn(c.getCreatedOn());
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, goodPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(ps1AtCreatedOn, ps2AtCreatedOn),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "createdOn differs for PatchSet.Id "
+ + c.getId()
+ + ",1: {2009-09-30 17:00:12.0} != {2009-09-30 17:00:00.0}",
+ "createdOn differs for PatchSet.Id "
+ + c.getId()
+ + ",2: {2009-09-30 17:00:18.0} != {2009-09-30 17:00:00.0}");
+
+ // One ReviewDb, PS1 is allowed to match change createdOn, but PS2 isn't.
+ b1 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, goodPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(ps1AtCreatedOn, ps2AtCreatedOn),
+ approvals(),
+ comments(),
+ reviewers(),
+ NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "createdOn differs for PatchSet.Id "
+ + c.getId()
+ + ",2 in NoteDb vs. ReviewDb: {2009-09-30 17:00:00.0} != {2009-09-30 17:00:18.0}");
+ assertDiffs(
+ b2,
+ b1,
+ "createdOn differs for PatchSet.Id "
+ + c.getId()
+ + ",2 in NoteDb vs. ReviewDb: {2009-09-30 17:00:00.0} != {2009-09-30 17:00:18.0}");
+ }
+
+ @Test
+ public void diffPatchSetApprovalKeySets() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ int id = c.getId().get();
+ PatchSetApproval a1 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+ (short) 1,
+ TimeUtil.nowTs());
+ PatchSetApproval a2 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Verified")),
+ (short) 1,
+ TimeUtil.nowTs());
+
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
+
+ assertDiffs(
+ b1,
+ b2,
+ "PatchSetApproval.Key sets differ:"
+ + " ["
+ + id
+ + "%2C1,100,Code-Review] only in A;"
+ + " ["
+ + id
+ + "%2C1,100,Verified] only in B");
+ }
+
+ @Test
+ public void diffPatchSetApprovals() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ PatchSetApproval a1 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+ (short) 1,
+ TimeUtil.nowTs());
+ PatchSetApproval a2 = clone(a1);
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
+
+ assertNoDiffs(b1, b2);
+
+ a2.setValue((short) -1);
+ assertDiffs(
+ b1,
+ b2,
+ "value differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review: {1} != {-1}");
+ }
+
+ @Test
+ public void diffPatchSetApprovalsMixedSourcesAllowsSlop() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ subWindowResolution();
+ PatchSetApproval a1 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+ (short) 1,
+ truncateToSecond(TimeUtil.nowTs()));
+ PatchSetApproval a2 = clone(a1);
+ a2.setGranted(TimeUtil.nowTs());
+
+ // Both are ReviewDb, exact timestamp match is required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "granted differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review:"
+ + " {2009-09-30 17:00:07.0} != {2009-09-30 17:00:08.0}");
+
+ // One NoteDb, slop is allowed.
+ b1 =
+ new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+
+ // But not too much slop.
+ superWindowResolution();
+ PatchSetApproval a3 = clone(a1);
+ a3.setGranted(TimeUtil.nowTs());
+ b1 =
+ new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(), reviewers(), NOTE_DB);
+ ChangeBundle b3 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a3), comments(), reviewers(), REVIEW_DB);
+ String msg =
+ "granted differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review in NoteDb vs. ReviewDb:"
+ + " {2009-09-30 17:00:07.0} != {2009-09-30 17:00:15.0}";
+ assertDiffs(b1, b3, msg);
+ assertDiffs(b3, b1, msg);
+ }
+
+ @Test
+ public void diffPatchSetApprovalsAllowsTruncatedTimestampInNoteDb() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ PatchSetApproval a1 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+ (short) 1,
+ c.getCreatedOn());
+ PatchSetApproval a2 = clone(a1);
+ a2.setGranted(
+ new Timestamp(
+ LocalDate.of(1900, Month.JANUARY, 1)
+ .atStartOfDay()
+ .atZone(ZoneId.of(TIMEZONE_ID))
+ .toInstant()
+ .toEpochMilli()));
+
+ // Both are ReviewDb, exact match is required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "granted differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review:"
+ + " {2009-09-30 17:00:00.0} != {1900-01-01 00:00:00.0}");
+
+ // Truncating NoteDb timestamp is allowed.
+ b1 =
+ new ChangeBundle(c, messages(), latest(c), approvals(a1), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
+ public void diffPatchSetApprovalsIgnoresPostSubmitBitOnZeroVote() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ c.setStatus(Change.Status.MERGED);
+ PatchSetApproval a1 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+ (short) 0,
+ TimeUtil.nowTs());
+ a1.setPostSubmit(false);
+ PatchSetApproval a2 = clone(a1);
+ a2.setPostSubmit(true);
+
+ // Both are ReviewDb, exact match is required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "postSubmit differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review:"
+ + " {false} != {true}");
+
+ // One NoteDb, postSubmit is ignored.
+ b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(c, messages(), latest(c), approvals(a2), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ // postSubmit is not ignored if vote isn't 0.
+ a1.setValue((short) 1);
+ a2.setValue((short) 1);
+ assertDiffs(
+ b1,
+ b2,
+ "postSubmit differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review:"
+ + " {false} != {true}");
+ assertDiffs(
+ b2,
+ b1,
+ "postSubmit differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review:"
+ + " {true} != {false}");
+ }
+
+ @Test
+ public void diffReviewers() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ Timestamp now = TimeUtil.nowTs();
+ ReviewerSet r1 = reviewers(REVIEWER, new Account.Id(1), now);
+ ReviewerSet r2 = reviewers(REVIEWER, new Account.Id(2), now);
+
+ ChangeBundle b1 =
+ new ChangeBundle(c, messages(), latest(c), approvals(), comments(), r1, REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(c, messages(), latest(c), approvals(), comments(), r2, REVIEW_DB);
+ assertNoDiffs(b1, b1);
+ assertNoDiffs(b2, b2);
+ assertDiffs(b1, b2, "reviewer sets differ: [1] only in A; [2] only in B");
+ }
+
+ @Test
+ public void diffReviewersIgnoresStateAndTimestamp() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ ReviewerSet r1 = reviewers(REVIEWER, new Account.Id(1), TimeUtil.nowTs());
+ ReviewerSet r2 = reviewers(CC, new Account.Id(1), TimeUtil.nowTs());
+
+ ChangeBundle b1 =
+ new ChangeBundle(c, messages(), latest(c), approvals(), comments(), r1, REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(c, messages(), latest(c), approvals(), comments(), r2, REVIEW_DB);
+ assertNoDiffs(b1, b1);
+ assertNoDiffs(b2, b2);
+ }
+
+ @Test
+ public void diffPatchLineCommentKeySets() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ int id = c.getId().get();
+ PatchLineComment c1 =
+ new PatchLineComment(
+ new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename1"), "uuid1"),
+ 5,
+ accountId,
+ null,
+ TimeUtil.nowTs());
+ PatchLineComment c2 =
+ new PatchLineComment(
+ new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename2"), "uuid2"),
+ 5,
+ accountId,
+ null,
+ TimeUtil.nowTs());
+
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c1), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c2), reviewers(), REVIEW_DB);
+
+ assertDiffs(
+ b1,
+ b2,
+ "PatchLineComment.Key sets differ:"
+ + " ["
+ + id
+ + ",1,filename1,uuid1] only in A;"
+ + " ["
+ + id
+ + ",1,filename2,uuid2] only in B");
+ }
+
+ @Test
+ public void diffPatchLineComments() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ PatchLineComment c1 =
+ new PatchLineComment(
+ new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename"), "uuid"),
+ 5,
+ accountId,
+ null,
+ TimeUtil.nowTs());
+ PatchLineComment c2 = clone(c1);
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c1), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c2), reviewers(), REVIEW_DB);
+
+ assertNoDiffs(b1, b2);
+
+ c2.setStatus(PatchLineComment.Status.PUBLISHED);
+ assertDiffs(
+ b1,
+ b2,
+ "status differs for PatchLineComment.Key " + c.getId() + ",1,filename,uuid: {d} != {P}");
+ }
+
+ @Test
+ public void diffPatchLineCommentsMixedSourcesAllowsSlop() throws Exception {
+ subWindowResolution();
+ Change c = TestChanges.newChange(project, accountId);
+ PatchLineComment c1 =
+ new PatchLineComment(
+ new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename"), "uuid"),
+ 5,
+ accountId,
+ null,
+ truncateToSecond(TimeUtil.nowTs()));
+ PatchLineComment c2 = clone(c1);
+ c2.setWrittenOn(TimeUtil.nowTs());
+
+ // Both are ReviewDb, exact timestamp match is required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c1), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c2), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "writtenOn differs for PatchLineComment.Key "
+ + c.getId()
+ + ",1,filename,uuid:"
+ + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:03.0}");
+
+ // One NoteDb, slop is allowed.
+ b1 =
+ new ChangeBundle(c, messages(), latest(c), approvals(), comments(c1), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c2), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+
+ // But not too much slop.
+ superWindowResolution();
+ PatchLineComment c3 = clone(c1);
+ c3.setWrittenOn(TimeUtil.nowTs());
+ b1 =
+ new ChangeBundle(c, messages(), latest(c), approvals(), comments(c1), reviewers(), NOTE_DB);
+ ChangeBundle b3 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c3), reviewers(), REVIEW_DB);
+ String msg =
+ "writtenOn differs for PatchLineComment.Key "
+ + c.getId()
+ + ",1,filename,uuid in NoteDb vs. ReviewDb:"
+ + " {2009-09-30 17:00:02.0} != {2009-09-30 17:00:10.0}";
+ assertDiffs(b1, b3, msg);
+ assertDiffs(b3, b1, msg);
+ }
+
+ @Test
+ public void diffPatchLineCommentsIgnoresCommentsOnInvalidPatchSet() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ PatchLineComment c1 =
+ new PatchLineComment(
+ new PatchLineComment.Key(new Patch.Key(c.currentPatchSetId(), "filename1"), "uuid1"),
+ 5,
+ accountId,
+ null,
+ TimeUtil.nowTs());
+ PatchLineComment c2 =
+ new PatchLineComment(
+ new PatchLineComment.Key(
+ new Patch.Key(new PatchSet.Id(c.getId(), 0), "filename2"), "uuid2"),
+ 5,
+ accountId,
+ null,
+ TimeUtil.nowTs());
+
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c1, c2), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(), comments(c1), reviewers(), REVIEW_DB);
+ assertNoDiffs(b1, b2);
+ }
+
+ private static void assertNoDiffs(ChangeBundle a, ChangeBundle b) {
+ assertThat(a.differencesFrom(b)).isEmpty();
+ assertThat(b.differencesFrom(a)).isEmpty();
+ }
+
+ private static void assertDiffs(ChangeBundle a, ChangeBundle b, String first, String... rest) {
+ List<String> actual = a.differencesFrom(b);
+ if (actual.size() == 1 && rest.length == 0) {
+ // This error message is much easier to read.
+ assertThat(actual.get(0)).isEqualTo(first);
+ } else {
+ List<String> expected = new ArrayList<>(1 + rest.length);
+ expected.add(first);
+ Collections.addAll(expected, rest);
+ assertThat(actual).containsExactlyElementsIn(expected).inOrder();
+ }
+ assertThat(a).isNotEqualTo(b);
+ }
+
+ private static List<ChangeMessage> messages(ChangeMessage... ents) {
+ return Arrays.asList(ents);
+ }
+
+ private static List<PatchSet> patchSets(PatchSet... ents) {
+ return Arrays.asList(ents);
+ }
+
+ private static List<PatchSet> latest(Change c) {
+ PatchSet ps = new PatchSet(c.currentPatchSetId());
+ ps.setCreatedOn(c.getLastUpdatedOn());
+ return ImmutableList.of(ps);
+ }
+
+ private static List<PatchSetApproval> approvals(PatchSetApproval... ents) {
+ return Arrays.asList(ents);
+ }
+
+ private static ReviewerSet reviewers(Object... ents) {
+ checkArgument(ents.length % 3 == 0);
+ Table<ReviewerStateInternal, Account.Id, Timestamp> t = HashBasedTable.create();
+ for (int i = 0; i < ents.length; i += 3) {
+ t.put((ReviewerStateInternal) ents[i], (Account.Id) ents[i + 1], (Timestamp) ents[i + 2]);
+ }
+ return ReviewerSet.fromTable(t);
+ }
+
+ private static List<PatchLineComment> comments(PatchLineComment... ents) {
+ return Arrays.asList(ents);
+ }
+
+ private static Change clone(Change ent) {
+ return clone(CHANGE_CODEC, ent);
+ }
+
+ private static ChangeMessage clone(ChangeMessage ent) {
+ return clone(CHANGE_MESSAGE_CODEC, ent);
+ }
+
+ private static PatchSet clone(PatchSet ent) {
+ return clone(PATCH_SET_CODEC, ent);
+ }
+
+ private static PatchSetApproval clone(PatchSetApproval ent) {
+ return clone(PATCH_SET_APPROVAL_CODEC, ent);
+ }
+
+ private static PatchLineComment clone(PatchLineComment ent) {
+ return clone(PATCH_LINE_COMMENT_CODEC, ent);
+ }
+
+ private static <T> T clone(ProtobufCodec<T> codec, T obj) {
+ return codec.decode(codec.encodeToByteArray(obj));
+ }
+}