mirror of
https://github.com/google/nomulus.git
synced 2025-07-08 20:23:24 +02:00
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:
parent
554e675303
commit
9e6f99face
15 changed files with 262 additions and 58 deletions
|
@ -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"));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue