Improve pretty-print diffing of Datastore entities

This removes the countless lines of the form "[null, []]" in registry_tool diffs
that are an artifact of the way we handle nulls in Objectify.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=133409440
This commit is contained in:
mcilwain 2016-09-16 11:43:38 -07:00 committed by Ben McIlwain
parent 841be34d18
commit aa7c05cb8b
6 changed files with 53 additions and 24 deletions

View file

@ -23,7 +23,7 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.emptyToNull; import static com.google.common.base.Strings.emptyToNull;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.DatastoreServiceUtils.getNameOrId; import static google.registry.util.DatastoreServiceUtils.getNameOrId;
import static google.registry.util.DiffUtils.prettyPrintDeepDiff; import static google.registry.util.DiffUtils.prettyPrintEntityDeepDiff;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
@ -101,16 +101,17 @@ public abstract class MutatingCommand extends ConfirmingCommand implements Remot
/** Returns a string representation of this entity change. */ /** Returns a string representation of this entity change. */
@Override @Override
public String toString() { public String toString() {
String changeText;
if (type == ChangeType.UPDATE) {
String diffText = prettyPrintEntityDeepDiff(
oldEntity.toDiffableFieldMap(), newEntity.toDiffableFieldMap());
changeText = Optional.fromNullable(emptyToNull(diffText)).or("[no changes]\n");
} else {
changeText = MoreObjects.firstNonNull(oldEntity, newEntity) + "\n";
}
return String.format( return String.format(
"%s %s\n%s", "%s %s\n%s",
UPPER_UNDERSCORE.to(UPPER_CAMEL, type.toString()), UPPER_UNDERSCORE.to(UPPER_CAMEL, type.toString()), getEntityId(), changeText);
getEntityId(),
type == ChangeType.UPDATE
? Optional
.fromNullable(emptyToNull(prettyPrintDeepDiff(
oldEntity.toDiffableFieldMap(), newEntity.toDiffableFieldMap())))
.or("[no changes]\n")
: (MoreObjects.firstNonNull(oldEntity, newEntity) + "\n"));
} }
} }
@ -153,7 +154,7 @@ public abstract class MutatingCommand extends ConfirmingCommand implements Remot
checkState( checkState(
Objects.equals(change.oldEntity, existingEntity), Objects.equals(change.oldEntity, existingEntity),
"Entity changed since init() was called.\n%s", "Entity changed since init() was called.\n%s",
prettyPrintDeepDiff( prettyPrintEntityDeepDiff(
change.oldEntity == null ? ImmutableMap.of() change.oldEntity == null ? ImmutableMap.of()
: change.oldEntity.toDiffableFieldMap(), : change.oldEntity.toDiffableFieldMap(),
existingEntity == null ? ImmutableMap.of() existingEntity == null ? ImmutableMap.of()

View file

@ -139,7 +139,7 @@ public class RegistrarServlet extends ResourceServlet {
String registrarName, String registrarName,
Map<String, Object> existingRegistrar, Map<String, Object> existingRegistrar,
Map<String, Object> updatedRegistrar) { Map<String, Object> updatedRegistrar) {
Map<?, ?> diffs = DiffUtils.deepDiff(existingRegistrar, updatedRegistrar); Map<?, ?> diffs = DiffUtils.deepDiff(existingRegistrar, updatedRegistrar, true);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Set<String> changedKeys = (Set<String>) diffs.keySet(); Set<String> changedKeys = (Set<String>) diffs.keySet();
if (CollectionUtils.difference(changedKeys, "lastUpdateTime").isEmpty()) { if (CollectionUtils.difference(changedKeys, "lastUpdateTime").isEmpty()) {

View file

@ -26,6 +26,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.common.primitives.Primitives; import com.google.common.primitives.Primitives;
import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -57,25 +58,37 @@ public final class DiffUtils {
} }
} }
/** Pretty-prints a deep diff between two maps. */ /** Pretty-prints a deep diff between two maps that represent Datastore entities. */
public static String prettyPrintDeepDiff(Map<?, ?> a, Map<?, ?> b) { public static String prettyPrintEntityDeepDiff(Map<?, ?> a, Map<?, ?> b) {
return prettyPrintDiffedMap(deepDiff(a, b), null); return prettyPrintDiffedMap(deepDiff(a, b, true), null);
} }
/** /**
* Pretty-prints a deep diff between two maps. Path is prefixed to each output line of the diff. * Pretty-prints a deep diff between two maps that represent XML documents. Path is prefixed to
* each output line of the diff.
*/ */
public static String prettyPrintDeepDiff(Map<?, ?> a, Map<?, ?> b, @Nullable String path) { public static String prettyPrintXmlDeepDiff(Map<?, ?> a, Map<?, ?> b, @Nullable String path) {
return prettyPrintDiffedMap(deepDiff(a, b), path); return prettyPrintDiffedMap(deepDiff(a, b, false), path);
} }
/** Compare two maps and return a map containing, at each key where they differed, both values. */ /** Compare two maps and return a map containing, at each key where they differed, both values. */
public static ImmutableMap<?, ?> deepDiff(Map<?, ?> a, Map<?, ?> b) { public static ImmutableMap<?, ?> deepDiff(
Map<?, ?> a, Map<?, ?> b, boolean ignoreNullToCollection) {
ImmutableMap.Builder<Object, Object> diff = new ImmutableMap.Builder<>(); ImmutableMap.Builder<Object, Object> diff = new ImmutableMap.Builder<>();
for (Object key : Sets.union(a.keySet(), b.keySet())) { for (Object key : Sets.union(a.keySet(), b.keySet())) {
Object aValue = a.get(key); Object aValue = a.get(key);
Object bValue = b.get(key); Object bValue = b.get(key);
if (!Objects.equals(aValue, bValue)) { if (Objects.equals(aValue, bValue)) {
// The objects are equal, so print nothing.
} else if (ignoreNullToCollection
&& aValue == null
&& bValue instanceof Collection
&& ((Collection<?>) bValue).isEmpty()) {
// Ignore a mismatch between Objectify's use of null to store empty collections and our
// code's builder methods, which yield empty collections for the same fields. This
// prevents useless lines of the form "[null, []]" from appearing in diffs.
} else {
// The objects aren't equal, so output a diff.
if (aValue instanceof String && bValue instanceof String if (aValue instanceof String && bValue instanceof String
&& a.toString().contains("\n") && b.toString().contains("\n")) { && a.toString().contains("\n") && b.toString().contains("\n")) {
aValue = stringToMap((String) aValue); aValue = stringToMap((String) aValue);
@ -87,7 +100,7 @@ public final class DiffUtils {
bValue = iterableToSortedMap((Iterable<?>) bValue); bValue = iterableToSortedMap((Iterable<?>) bValue);
} }
diff.put(key, (aValue instanceof Map && bValue instanceof Map) diff.put(key, (aValue instanceof Map && bValue instanceof Map)
? deepDiff((Map<?, ?>) aValue, (Map<?, ?>) bValue) ? deepDiff((Map<?, ?>) aValue, (Map<?, ?>) bValue, ignoreNullToCollection)
: new DiffPair(aValue, bValue)); : new DiffPair(aValue, bValue));
} }
} }

View file

@ -23,7 +23,7 @@ import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Multisets.containsOccurrences; import static com.google.common.collect.Multisets.containsOccurrences;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_; import static com.google.common.truth.Truth.assert_;
import static google.registry.util.DiffUtils.prettyPrintDeepDiff; import static google.registry.util.DiffUtils.prettyPrintEntityDeepDiff;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@ -227,7 +227,7 @@ public class TaskQueueHelper {
.transform(new Function<TaskStateInfo, String>() { .transform(new Function<TaskStateInfo, String>() {
@Override @Override
public String apply(TaskStateInfo input) { public String apply(TaskStateInfo input) {
return prettyPrintDeepDiff( return prettyPrintEntityDeepDiff(
taskMatcherMap, taskMatcherMap,
Maps.filterKeys( Maps.filterKeys(
new MatchableTaskInfo(input).toMap(), new MatchableTaskInfo(input).toMap(),

View file

@ -15,9 +15,12 @@
package google.registry.util; package google.registry.util;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.util.DiffUtils.prettyPrintEntityDeepDiff;
import static google.registry.util.DiffUtils.prettyPrintSetDiff; import static google.registry.util.DiffUtils.prettyPrintSetDiff;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
@ -57,6 +60,18 @@ public class DiffUtilsTest {
.isEqualTo("\n ADDED: [y, z]\n REMOVED: [b, c]\n FINAL CONTENTS: [a, y, z]"); .isEqualTo("\n ADDED: [y, z]\n REMOVED: [b, c]\n FINAL CONTENTS: [a, y, z]");
} }
@Test
public void test_emptyToNullCollection_doesntDisplay() {
Map<String, Object> mapA = new HashMap<String, Object>();
mapA.put("a", "jim");
mapA.put("b", null);
Map<String, Object> mapB = new HashMap<String, Object>();
mapB.put("a", "tim");
mapB.put("b", ImmutableSet.of());
// This ensures that it is not outputting a diff of [b -> [null, []].
assertThat(prettyPrintEntityDeepDiff(mapA, mapB)).isEqualTo("a -> [jim, tim]\n");
}
@Test @Test
public void test_prettyPrintSetDiff_addedAndRemovedElements_objects() { public void test_prettyPrintSetDiff_addedAndRemovedElements_objects() {
DummyObject a = DummyObject.create("a"); DummyObject a = DummyObject.create("a");

View file

@ -16,7 +16,7 @@ package google.registry.xml;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Truth.assert_; import static com.google.common.truth.Truth.assert_;
import static google.registry.util.DiffUtils.prettyPrintDeepDiff; import static google.registry.util.DiffUtils.prettyPrintXmlDeepDiff;
import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.DateTimeZone.UTC;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
@ -59,7 +59,7 @@ public class XmlTestUtils {
message, message,
expected, expected,
actual, actual,
prettyPrintDeepDiff(expectedMap, actualMap, null))); prettyPrintXmlDeepDiff(expectedMap, actualMap, null)));
} }
} }