Add object comparison to replay tests (#925)

* Add object comparison to replay tests

Allow optional object comparison in the replay test extension and enable it
for the DomainCreateFlow test.

To faciliate this, add two new field annotations to ImmutableObject:
DoNotCompare, to be used for fields that are not relevant to either database,
and Insignificant, to be used for fields that are mutated after they have been
accessed and therefore violate immutability (there is currently only one of
these, however we might discover more in the course of adding more comparisons
to the replay test.

* Revert commented out premium price error log

* Added static create methods for ReplayExtension
This commit is contained in:
Michael Muller 2021-01-15 14:20:55 -05:00 committed by GitHub
parent 554e675303
commit 9e6f99face
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 262 additions and 58 deletions

View file

@ -184,7 +184,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithCompare(clock);
DomainCreateFlowTest() {
setEppInput("domain_create.xml", ImmutableMap.of("DOMAIN", "example.tld"));

View file

@ -112,7 +112,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
private DomainBase domain;
private HistoryEntry earlierHistoryEntry;

View file

@ -108,7 +108,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
@BeforeEach
void initDomainTest() {

View file

@ -117,7 +117,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = new ReplayExtension(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
@BeforeEach
void initDomainTest() {

View file

@ -23,10 +23,12 @@ import com.google.common.truth.Correspondence.BinaryPredicate;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.SimpleSubjectBuilder;
import com.google.common.truth.Subject;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
/** Truth subject for asserting things about ImmutableObjects that are not built in. */
@ -53,6 +55,26 @@ public final class ImmutableObjectSubject extends Subject {
}
}
/**
* Checks that {@code expected} has the same contents as {@code actual} except for fields that are
* marked with {@link ImmutableObject.DoNotCompare}.
*
* <p>This is used to verify that entities stored in both cloud SQL and Datastore are identical.
*/
public void isEqualAcrossDatabases(@Nullable ImmutableObject expected) {
if (actual == null) {
assertThat(expected).isNull();
} else {
assertThat(expected).isNotNull();
}
if (actual != null) {
Map<Field, Object> actualFields = filterFields(actual, ImmutableObject.DoNotCompare.class);
Map<Field, Object> expectedFields =
filterFields(expected, ImmutableObject.DoNotCompare.class);
assertThat(actualFields).containsExactlyEntriesIn(expectedFields);
}
}
public static Correspondence<ImmutableObject, ImmutableObject> immutableObjectCorrespondence(
String... ignoredFields) {
return Correspondence.from(
@ -99,4 +121,26 @@ public final class ImmutableObjectSubject extends Subject {
}
return result;
}
/** Filter out fields with the given annotation. */
public static Map<Field, Object> filterFields(
ImmutableObject original, Class<? extends Annotation> annotation) {
Map<Field, Object> originalFields = ModelUtils.getFieldValues(original);
// don't use ImmutableMap or a stream->collect model since we can have nulls
Map<Field, Object> result = new LinkedHashMap<>();
for (Map.Entry<Field, Object> entry : originalFields.entrySet()) {
if (!entry.getKey().isAnnotationPresent(annotation)) {
// Perform any necessary substitutions.
if (entry.getKey().isAnnotationPresent(ImmutableObject.EmptySetToNull.class)
&& entry.getValue() != null
&& ((Set<?>) entry.getValue()).isEmpty()) {
result.put(entry.getKey(), null);
} else {
result.put(entry.getKey(), entry.getValue());
}
}
}
return result;
}
}

View file

@ -14,7 +14,19 @@
package google.registry.testing;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.model.ImmutableObject;
import google.registry.model.ofy.ReplayQueue;
import google.registry.model.ofy.TransactionInfo;
import google.registry.persistence.VKey;
import java.util.Optional;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
@ -26,13 +38,26 @@ import org.junit.jupiter.api.extension.ExtensionContext;
* that extension are also replayed. If AppEngineExtension is not used,
* JpaTransactionManagerExtension must be, and this extension should be ordered _after_
* JpaTransactionManagerExtension so that writes to SQL work.
*
* <p>If the "compare" flag is set in the constructor, this will also compare all touched objects in
* both databases after performing the replay.
*/
public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
FakeClock clock;
boolean compare;
public ReplayExtension(FakeClock clock) {
private ReplayExtension(FakeClock clock, boolean compare) {
this.clock = clock;
this.compare = compare;
}
public static ReplayExtension createWithCompare(FakeClock clock) {
return new ReplayExtension(clock, true);
}
public static ReplayExtension createWithoutCompare(FakeClock clock) {
return new ReplayExtension(clock, false);
}
@Override
@ -51,8 +76,48 @@ public class ReplayExtension implements BeforeEachCallback, AfterEachCallback {
replayToSql();
}
private static ImmutableSet<String> NON_REPLICATED_TYPES =
ImmutableSet.of(
"PremiumList",
"PremiumListRevision",
"PremiumListEntry",
"ReservedList",
"RdeRevision",
"KmsSecretRevision",
"ServerSecret",
"SignedMarkRevocationList",
"ClaimsListShard",
"TmchCrl",
"EppResourceIndex",
"ForeignKeyIndex",
"ForeignKeyHostIndex",
"ForeignKeyContactIndex",
"ForeignKeyDomainIndex");
public void replayToSql() {
DatabaseHelper.setAlwaysSaveWithBackup(false);
ReplayQueue.replay();
ImmutableMap<Key<?>, Object> changes = ReplayQueue.replay();
// Compare JPA to OFY, if requested.
if (compare) {
for (ImmutableMap.Entry<Key<?>, Object> entry : changes.entrySet()) {
// Don't verify non-replicated types.
if (NON_REPLICATED_TYPES.contains(entry.getKey().getKind())) {
continue;
}
VKey<?> vkey = VKey.from(entry.getKey());
Optional<?> ofyValue = ofyTm().transact(() -> ofyTm().loadByKeyIfPresent(vkey));
Optional<?> jpaValue = jpaTm().transact(() -> jpaTm().loadByKeyIfPresent(vkey));
if (entry.getValue().equals(TransactionInfo.Delete.SENTINEL)) {
assertThat(jpaValue.isPresent()).isFalse();
assertThat(ofyValue.isPresent()).isFalse();
} else {
assertAboutImmutableObjects()
.that((ImmutableObject) jpaValue.get())
.isEqualAcrossDatabases((ImmutableObject) ofyValue.get());
}
}
}
}
}