Add helper for comparing a bundle of Change-related entities

Currently this just does a field-by-field comparison of all entities
rooted at a single change. Eventually it will understand the subtle
differences between ReviewDb and NoteDb (e.g. timestamp rounding), and
will be used for consistency checks before/after migration.

Change-Id: I8bd97bcee7a907bed2126aaf6f77bba993374884
This commit is contained in:
Dave Borowitz
2016-02-24 16:31:39 -05:00
parent 59da717398
commit b0cfc534c5
4 changed files with 734 additions and 15 deletions

View File

@@ -55,21 +55,6 @@ public final class PatchLineComment {
public void set(String newValue) {
uuid = newValue;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("PatchLineComment.Key{");
builder.append("Change.Id=")
.append(getParentKey().getParentKey().getParentKey().get()).append(',');
builder.append("PatchSet.Id=")
.append(getParentKey().getParentKey().get()).append(',');
builder.append("filename=")
.append(getParentKey().getFileName()).append(',');
builder.append("uuid=").append(get());
builder.append("}");
return builder.toString();
}
}
public static final char STATUS_DRAFT = 'd';

View File

@@ -198,6 +198,7 @@ java_test(
'//lib:grappa',
'//lib:guava',
'//lib:guava-retrying',
'//lib:protobuf',
'//lib/dropwizard:dropwizard-core',
'//lib/guice:guice-assistedinject',
'//lib/prolog:runtime',

View File

@@ -0,0 +1,366 @@
// 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.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
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.server.ReviewDb;
import com.google.gerrit.server.PatchLineCommentsUtil;
import com.google.gwtorm.client.Column;
import com.google.gwtorm.server.OrmException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* A bundle of all entities rooted at a single {@link Change} entity.
* <p>
* See the {@link Change} Javadoc for a depiction of this tree. Bundles may be
* compared using {@link #differencesFrom(ChangeBundle)}, which normalizes out
* the minor implementation differences between ReviewDb and NoteDb.
*/
public class ChangeBundle {
public static ChangeBundle fromReviewDb(ReviewDb db, Change.Id id)
throws OrmException {
db.changes().beginTransaction(id);
try {
return new ChangeBundle(
db.changes().get(id),
db.changeMessages().byChange(id),
db.patchSets().byChange(id),
db.patchSetApprovals().byChange(id),
db.patchComments().byChange(id));
} finally {
db.rollback();
}
}
public static ChangeBundle fromNotes(PatchLineCommentsUtil plcUtil,
ChangeNotes notes) throws OrmException {
return new ChangeBundle(
notes.getChange(),
notes.getChangeMessages(),
notes.getPatchSets().values(),
notes.getApprovals().values(),
Iterables.concat(
plcUtil.draftByChange(null, notes),
plcUtil.publishedByChange(null, notes)));
}
private static Map<ChangeMessage.Key, ChangeMessage> changeMessageMap(
Iterable<ChangeMessage> in) {
Map<ChangeMessage.Key, ChangeMessage> out = new TreeMap<>(
new Comparator<ChangeMessage.Key>() {
@Override
public int compare(ChangeMessage.Key a, ChangeMessage.Key b) {
return ComparisonChain.start()
.compare(a.getParentKey().get(), b.getParentKey().get())
.compare(a.get(), b.get())
.result();
}
});
for (ChangeMessage cm : in) {
out.put(cm.getKey(), cm);
}
return out;
}
private static Map<PatchSet.Id, PatchSet> patchSetMap(Iterable<PatchSet> in) {
Map<PatchSet.Id, PatchSet> out = new TreeMap<>(
new Comparator<PatchSet.Id>() {
@Override
public int compare(PatchSet.Id a, PatchSet.Id b) {
return patchSetIdChain(a, b).result();
}
});
for (PatchSet ps : in) {
out.put(ps.getId(), ps);
}
return out;
}
private static Map<PatchSetApproval.Key, PatchSetApproval>
patchSetApprovalMap(Iterable<PatchSetApproval> in) {
Map<PatchSetApproval.Key, PatchSetApproval> out = new TreeMap<>(
new Comparator<PatchSetApproval.Key>() {
@Override
public int compare(PatchSetApproval.Key a, PatchSetApproval.Key b) {
return patchSetIdChain(a.getParentKey(), b.getParentKey())
.compare(a.getAccountId().get(), b.getAccountId().get())
.compare(a.getLabelId(), b.getLabelId())
.result();
}
});
for (PatchSetApproval psa : in) {
out.put(psa.getKey(), psa);
}
return out;
}
private static Map<PatchLineComment.Key, PatchLineComment>
patchLineCommentMap(Iterable<PatchLineComment> in) {
Map<PatchLineComment.Key, PatchLineComment> out = new TreeMap<>(
new Comparator<PatchLineComment.Key>() {
@Override
public int compare(PatchLineComment.Key a, PatchLineComment.Key b) {
Patch.Key pka = a.getParentKey();
Patch.Key pkb = b.getParentKey();
return patchSetIdChain(pka.getParentKey(), pkb.getParentKey())
.compare(pka.get(), pkb.get())
.compare(a.get(), b.get())
.result();
}
});
for (PatchLineComment plc : in) {
out.put(plc.getKey(), plc);
}
return out;
}
private static ComparisonChain patchSetIdChain(PatchSet.Id a, PatchSet.Id b) {
return ComparisonChain.start()
.compare(a.getParentKey().get(), b.getParentKey().get())
.compare(a.get(), b.get());
}
private static void checkColumns(Class<?> clazz, Integer... expected) {
Set<Integer> ids = new TreeSet<>();
for (Field f : clazz.getDeclaredFields()) {
Column col = f.getAnnotation(Column.class);
if (col != null) {
ids.add(col.id());
}
}
Set<Integer> expectedIds = Sets.newTreeSet(Arrays.asList(expected));
checkState(ids.equals(expectedIds),
"Unexpected column set for %s: %s != %s",
clazz.getSimpleName(), ids, expectedIds);
}
static {
// Initialization-time checks that the column set hasn't changed since the
// last time this file was updated.
checkColumns(Change.Id.class, 1);
checkColumns(Change.class, 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18);
checkColumns(ChangeMessage.Key.class, 1, 2);
checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5);
checkColumns(PatchSet.Id.class, 1, 2);
checkColumns(PatchSet.class, 1, 2, 3, 4, 5, 6, 8);
checkColumns(PatchSetApproval.Key.class, 1, 2, 3);
checkColumns(PatchSetApproval.class, 1, 2, 3);
checkColumns(PatchLineComment.Key.class, 1, 2);
checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9);
}
private final Change change;
private final ImmutableMap<ChangeMessage.Key, ChangeMessage> changeMessages;
private final ImmutableMap<PatchSet.Id, PatchSet> patchSets;
private final ImmutableMap<PatchSetApproval.Key, PatchSetApproval>
patchSetApprovals;
private final ImmutableMap<PatchLineComment.Key, PatchLineComment>
patchLineComments;
@VisibleForTesting
ChangeBundle(
Change change,
Iterable<ChangeMessage> changeMessages,
Iterable<PatchSet> patchSets,
Iterable<PatchSetApproval> patchSetApprovals,
Iterable<PatchLineComment> patchLineComments) {
this.change = checkNotNull(change);
this.changeMessages = ImmutableMap.copyOf(changeMessageMap(changeMessages));
this.patchSets = ImmutableMap.copyOf(patchSetMap(patchSets));
this.patchSetApprovals =
ImmutableMap.copyOf(patchSetApprovalMap(patchSetApprovals));
this.patchLineComments =
ImmutableMap.copyOf(patchLineCommentMap(patchLineComments));
for (ChangeMessage.Key k : this.changeMessages.keySet()) {
checkArgument(k.getParentKey().equals(change.getId()));
}
for (PatchSet.Id id : this.patchSets.keySet()) {
checkArgument(id.getParentKey().equals(change.getId()));
}
for (PatchSetApproval.Key k : this.patchSetApprovals.keySet()) {
checkArgument(k.getParentKey().getParentKey().equals(change.getId()));
}
for (PatchLineComment.Key k : this.patchLineComments.keySet()) {
checkArgument(k.getParentKey().getParentKey().getParentKey()
.equals(change.getId()));
}
}
public ImmutableList<String> differencesFrom(ChangeBundle o) {
List<String> diffs = new ArrayList<>();
diffChanges(diffs, this, o);
diffChangeMessages(diffs, this, o);
diffPatchSets(diffs, this, o);
diffPatchSetApprovals(diffs, this, o);
diffPatchLineComments(diffs, this, o);
return ImmutableList.copyOf(diffs);
}
private static void diffChanges(List<String> diffs, ChangeBundle bundleA,
ChangeBundle bundleB) {
Change a = bundleA.change;
Change b = bundleB.change;
String desc = a.getId().equals(b.getId()) ? describe(a.getId()) : "Changes";
diffColumns(diffs, Change.class, desc, a, b);
}
private static void diffChangeMessages(List<String> diffs,
ChangeBundle bundleA, ChangeBundle bundleB) {
Map<ChangeMessage.Key, ChangeMessage> as = bundleA.changeMessages;
Map<ChangeMessage.Key, ChangeMessage> bs = bundleB.changeMessages;
for (ChangeMessage.Key k : diffKeySets(diffs, as, bs)) {
diffColumns(
diffs, ChangeMessage.class, describe(k), as.get(k), bs.get(k));
}
}
private static void diffPatchSets(List<String> diffs, ChangeBundle bundleA,
ChangeBundle bundleB) {
Map<PatchSet.Id, PatchSet> as = bundleA.patchSets;
Map<PatchSet.Id, PatchSet> bs = bundleB.patchSets;
for (PatchSet.Id id : diffKeySets(diffs, as, bs)) {
diffColumns(diffs, PatchSet.class, describe(id), as.get(id), bs.get(id));
}
}
private static void diffPatchSetApprovals(List<String> diffs,
ChangeBundle bundleA, ChangeBundle bundleB) {
Map<PatchSetApproval.Key, PatchSetApproval> as = bundleA.patchSetApprovals;
Map<PatchSetApproval.Key, PatchSetApproval> bs = bundleB.patchSetApprovals;
for (PatchSetApproval.Key k : diffKeySets(diffs, as, bs)) {
diffColumns(
diffs, PatchSetApproval.class, describe(k), as.get(k), bs.get(k));
}
}
private static void diffPatchLineComments(List<String> diffs,
ChangeBundle bundleA, ChangeBundle bundleB) {
Map<PatchLineComment.Key, PatchLineComment> as = bundleA.patchLineComments;
Map<PatchLineComment.Key, PatchLineComment> bs = bundleB.patchLineComments;
for (PatchLineComment.Key k : diffKeySets(diffs, as, bs)) {
diffColumns(
diffs, PatchLineComment.class, describe(k), as.get(k), bs.get(k));
}
}
private static <T> Set<T> diffKeySets(List<String> diffs, Map<T, ?> a,
Map<T, ?> b) {
Set<T> as = a.keySet();
Set<T> bs = b.keySet();
if (as.isEmpty() && bs.isEmpty()) {
return as;
}
String clazz = keyClass((!as.isEmpty() ? as : bs).iterator().next());
Set<T> aNotB = Sets.difference(as, bs);
Set<T> bNotA = Sets.difference(bs, as);
if (aNotB.isEmpty() && bNotA.isEmpty()) {
return as;
}
diffs.add(clazz + " sets differ: " + aNotB + " only in A; "
+ bNotA + " only in B");
return Sets.intersection(as, bs);
}
private static <T> void diffColumns(List<String> diffs, Class<T> clazz,
String desc, T a, T b) {
for (Field f : clazz.getDeclaredFields()) {
Column col = f.getAnnotation(Column.class);
if (col == null) {
continue;
}
f.setAccessible(true);
try {
diffValues(diffs, desc, f.get(a), f.get(b), f.getName());
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
}
}
private static void diffValues(List<String> diffs, String desc, Object va,
Object vb, String name) {
if (!Objects.equals(va, vb)) {
diffs.add(
name + " differs for " + desc + ": {" + va + "} != {" + vb + "}");
}
}
private static String describe(Object key) {
return keyClass(key) + " " + key;
}
private static String keyClass(Object obj) {
Class<?> clazz = obj.getClass();
String name = clazz.getSimpleName();
checkArgument(name.equals("Key") || name.equals("Id"),
"not an Id/Key class: %s", name);
return clazz.getEnclosingClass().getSimpleName() + "." + name;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ChangeBundle)) {
return false;
}
return differencesFrom((ChangeBundle) o).isEmpty();
}
@Override
public int hashCode() {
return Objects.hash(
change.getId(),
changeMessages.keySet(),
patchSets.keySet(),
patchSetApprovals.keySet(),
patchLineComments.keySet());
}
@Override
public String toString() {
return getClass().getSimpleName() + "{id=" + change.getId()
+ ", ChangeMessage[" + changeMessages.size() + "]"
+ ", PatchSet[" + patchSets.size() + "]"
+ ", PatchSetApproval[" + patchSetApprovals.size() + "]"
+ ", PatchLineComment[" + patchLineComments.size() + "]"
+ "}";
}
}

View File

@@ -0,0 +1,367 @@
// 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.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.testutil.TestChanges;
import com.google.gerrit.testutil.TestTimeUtil;
import com.google.gwtorm.client.KeyUtil;
import com.google.gwtorm.protobuf.CodecFactory;
import com.google.gwtorm.protobuf.ProtobufCodec;
import com.google.gwtorm.server.StandardKeyEncoder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;
public class ChangeBundleTest {
static {
KeyUtil.setEncoderImpl(new StandardKeyEncoder());
}
private static final ProtobufCodec<Change> CHANGE_CODEC =
CodecFactory.encoder(Change.class);
private static final ProtobufCodec<ChangeMessage> CHANGE_MESSAGE_CODEC =
CodecFactory.encoder(ChangeMessage.class);
private static final ProtobufCodec<PatchSet> PATCH_SET_CODEC =
CodecFactory.encoder(PatchSet.class);
private static final ProtobufCodec<PatchSetApproval>
PATCH_SET_APPROVAL_CODEC = CodecFactory.encoder(PatchSetApproval.class);
private static final ProtobufCodec<PatchLineComment>
PATCH_LINE_COMMENT_CODEC = CodecFactory.encoder(PatchLineComment.class);
private String systemTimeZoneProperty;
private TimeZone systemTimeZone;
private Project.NameKey project;
private Account.Id accountId;
@Before
public void setUp() {
String tz = "US/Eastern";
systemTimeZoneProperty = System.setProperty("user.timezone", tz);
systemTimeZone = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone(tz));
TestTimeUtil.resetWithClockStep(1, SECONDS);
project = new Project.NameKey("project");
accountId = new Account.Id(100);
}
@After
public void tearDown() {
TestTimeUtil.useSystemTime();
System.setProperty("user.timezone", systemTimeZoneProperty);
TimeZone.setDefault(systemTimeZone);
}
@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());
ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
comments());
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:01.0}",
"lastUpdatedOn differs for Changes:"
+ " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:01.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());
ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), approvals(),
comments());
assertNoDiffs(b1, b2);
c2.setTopic("topic");
assertDiffs(b1, b2,
"topic differs for Change.Id "+ c1.getId() + ": {null} != {topic}");
}
@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), patchSets(),
approvals(), comments());
ChangeBundle b2 = new ChangeBundle(c, messages(cm2), patchSets(),
approvals(), comments());
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), patchSets(),
approvals(), comments());
ChangeBundle b2 = new ChangeBundle(c, messages(cm2), patchSets(),
approvals(), comments());
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 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());
ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2),
approvals(), comments());
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());
ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps2),
approvals(), comments());
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 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(), patchSets(),
approvals(a1), comments());
ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
approvals(a2), comments());
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(), patchSets(),
approvals(a1), comments());
ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
approvals(a2), comments());
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 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(), patchSets(),
approvals(), comments(c1));
ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
approvals(), comments(c2));
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(), patchSets(),
approvals(), comments(c1));
ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(),
approvals(), comments(c2));
assertNoDiffs(b1, b2);
c2.setStatus(PatchLineComment.Status.PUBLISHED);
assertDiffs(b1, b2,
"status differs for PatchLineComment.Key "
+ c.getId() + ",1,filename,uuid: {d} != {P}");
}
private static void assertNoDiffs(ChangeBundle a, ChangeBundle b) {
assertThat(a.differencesFrom(b)).isEmpty();
assertThat(b.differencesFrom(a)).isEmpty();
assertThat(a).isEqualTo(b);
assertThat(b).isEqualTo(a);
assertThat(a.hashCode()).isEqualTo(b.hashCode());
}
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<PatchSetApproval> approvals(PatchSetApproval... ents) {
return Arrays.asList(ents);
}
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));
}
}