google-nomulus/javatests/google/registry/model/EntityTestCase.java
shikhman f76bc70f91 Preserve test logs and test summary output for Kokoro CI runs
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=135494972
2016-10-14 16:57:43 -04:00

186 lines
8.1 KiB
Java

// Copyright 2016 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.model;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth.assert_;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.annotation.Serialize;
import com.googlecode.objectify.cmd.Query;
import google.registry.model.ofy.Ofy;
import google.registry.testing.AppEngineRule;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectRule;
import google.registry.util.CidrAddressBlock;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Set;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Base class of all unit tests for entities which are persisted to Datastore via Objectify. */
@RunWith(JUnit4.class)
public class EntityTestCase {
@Rule
public final AppEngineRule appEngine = AppEngineRule.builder()
.withDatastore()
.build();
@Rule
public InjectRule inject = new InjectRule();
protected FakeClock clock = new FakeClock(DateTime.now(UTC));
@Before
public void injectClock() throws Exception {
inject.setStaticField(Ofy.class, "clock", clock);
}
// Helper method to find private fields including inherited ones.
private Field getField(Class<?> clazz, String name) {
while (clazz != Object.class) {
try {
return clazz.getDeclaredField(name);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
/** Verify that fields are either indexed or not, depending on the parameter. */
private void verifyIndexingHelper(
Object obj,
boolean indexed,
Collection<String> fieldPaths) throws Exception {
outer: for (String fieldPath : fieldPaths) {
// Walk the field path and grab the value referred to on the object using reflection.
Object fieldValue = obj;
for (String fieldName : fieldPath.split("\\.")) {
if (fieldValue == null) {
throw new RuntimeException(String.format("field '%s' not found on %s",
fieldPath, obj.getClass().getSimpleName()));
}
Field field = getField(fieldValue.getClass(), fieldName);
field.setAccessible(true);
// Check if the field is annotated with @Id. If it is, it's always indexed. Although we
// might want to double check that fact, it's a bad idea, because filtering on a @Id field
// will fail if the class also contains an @Parent field.
if (field.isAnnotationPresent(Id.class)) {
continue outer;
}
fieldValue = field.get(fieldValue);
// Check if the field is a collection. If so, take the first value from the collection,
// since we can't query on collections themselves.
if (fieldValue instanceof Collection<?>) {
fieldValue = ((Collection<?>) fieldValue).iterator().next();
}
// Same for an array.
if (fieldValue != null && fieldValue.getClass().isArray()) {
fieldValue = Array.getLength(fieldValue) == 0 ? null : Array.get(fieldValue, 0);
}
// CidrAddressBlock objects implement the Iterable<InetAddress> interface, which makes
// Objectify think we're querying for a list (which is not allowed unless we use the IN
// keyword). Convert the object to a String to make sure we do a query for a single value.
if (fieldValue instanceof CidrAddressBlock) {
fieldValue = fieldValue.toString();
}
}
try {
// Objectify happily filters on an unindexed field, and just returns zero results.
// Do a query for that value and verify that the expected number of results are returned.
Query<?> query = ofy().load().type(obj.getClass());
int results = query.filter(fieldPath, fieldValue).count();
assertWithMessage(String.format("%s was %sindexed", fieldPath, indexed ? "not " : ""))
.that(indexed)
.isEqualTo(results != 0);
} catch (IllegalArgumentException e) {
// If the field's type was not indexable (because it's not a supported datastore type) then
// this error will be thrown. If we expected no indexing, that's fine. Otherwise, fail.
if (indexed || !e.getMessage().endsWith(" is not a supported property type.")) {
assert_().fail(String.format("%s was %sindexed", fieldPath, indexed ? "not " : ""));
}
} catch (IllegalStateException e) {
assert_().fail(String.format("%s (indexed=%b): %s", fieldPath, indexed, e.getMessage()));
}
}
}
/** List all field paths so we can verify that we checked everything. */
private Set<String> getAllPotentiallyIndexedFieldPaths(Class<?> clazz) {
Set<String> fields = Sets.newHashSet();
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
for (final Field field : clazz.getDeclaredFields()) {
// Ignore static, @Serialize, @Parent and @Ignore fields, since these are never indexed.
if (!Modifier.isStatic(field.getModifiers())
&& !field.isAnnotationPresent(Ignore.class)
&& !field.isAnnotationPresent(Parent.class)
&& !field.isAnnotationPresent(Serialize.class)) {
Class<?> fieldClass = field.getType();
// If the field is a collection, just pretend that it was a field of that type,
// because verifyIndexingHelper knows how to descend into collections.
if (Collection.class.isAssignableFrom(fieldClass)) {
Type inner = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
fieldClass = inner instanceof ParameterizedType
? (Class<?>) ((ParameterizedType) inner).getRawType()
: (Class<?>) inner;
}
// Descend into persisted ImmutableObject classes, but not anything else.
if (ImmutableObject.class.isAssignableFrom(fieldClass)) {
fields.addAll(FluentIterable
.from(getAllPotentiallyIndexedFieldPaths(fieldClass))
.transform(new Function<String, String>(){
@Override
public String apply(String subfield) {
return field.getName() + "." + subfield;
}})
.toSet());
} else {
fields.add(field.getName());
}
}
}
}
return fields;
}
/** Verify indexing for an entity. */
public void verifyIndexing(Object obj, String... indexed) throws Exception {
Set<String> indexedSet = ImmutableSet.copyOf(indexed);
Set<String> allSet = getAllPotentiallyIndexedFieldPaths(obj.getClass());
// Sanity test that the indexed fields we listed were found.
assertThat(Sets.intersection(allSet, indexedSet)).containsExactlyElementsIn(indexedSet);
verifyIndexingHelper(obj, true, indexedSet);
verifyIndexingHelper(obj, false, Sets.difference(allSet, indexedSet));
}
}