// 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.common.TimeUtil.roundToSecond; 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 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.common.TimeUtil; 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.testutil.GerritBaseTests; import com.google.gerrit.testutil.TestChanges; import com.google.gerrit.testutil.TestTimeUtil; import com.google.gwtorm.protobuf.CodecFactory; import com.google.gwtorm.protobuf.ProtobufCodec; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.TimeZone; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; 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 String systemTimeZoneProperty; private TimeZone systemTimeZone; private Project.NameKey project; private Account.Id accountId; @Before public void setUp() { String tz = "US/Eastern"; systemTimeZoneProperty = System.setProperty("user.timezone", tz); systemTimeZone = TimeZone.getDefault(); TimeZone.setDefault(TimeZone.getTimeZone(tz)); 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 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(roundToSecond(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(roundToSecond(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 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, roundToSecond(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( new DateTime(1900, 1, 1, 0, 0, 0, DateTimeZone.forTimeZone(TimeZone.getDefault())) .getMillis())); // 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, roundToSecond(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)); } }