| // 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)); |
| } |
| } |