blob: fc2a272082005b4af4544b8e57a1839c80ec95fa [file] [log] [blame]
// 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));
}
}