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:
@@ -55,21 +55,6 @@ public final class PatchLineComment {
|
|||||||
public void set(String newValue) {
|
public void set(String newValue) {
|
||||||
uuid = 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';
|
public static final char STATUS_DRAFT = 'd';
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ java_test(
|
|||||||
'//lib:grappa',
|
'//lib:grappa',
|
||||||
'//lib:guava',
|
'//lib:guava',
|
||||||
'//lib:guava-retrying',
|
'//lib:guava-retrying',
|
||||||
|
'//lib:protobuf',
|
||||||
'//lib/dropwizard:dropwizard-core',
|
'//lib/dropwizard:dropwizard-core',
|
||||||
'//lib/guice:guice-assistedinject',
|
'//lib/guice:guice-assistedinject',
|
||||||
'//lib/prolog:runtime',
|
'//lib/prolog:runtime',
|
||||||
|
|||||||
@@ -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() + "]"
|
||||||
|
+ "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user