blob: a45482f3006b38e245f5a6dc6bfe5a37fae6d7f2 [file] [log] [blame]
/*
* Copyright (C) 2017 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.android.build.gradle.internal.dsl
import com.android.build.gradle.internal.api.dsl.DslScope
import com.android.build.gradle.internal.api.dsl.sealing.SealableObject
import com.android.builder.errors.EvalIssueReporter
import com.google.common.truth.Truth
import org.gradle.api.JavaVersion
import org.junit.Assert
import org.mockito.Mockito
import java.io.File
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Modifier
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.KVisibility
import kotlin.reflect.full.allSuperclasses
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.jvmErasure
/**
* Utility class to test a {@link SealableObject} implementation :
*
* Current list of checks are :
* <ul>
* <li>All non final fields are declared as Kotlin Properties.</li>
* <li>All fields are either immutable (basic types or immutable types like File)
* or a SealableObject</li>
* <li>All non final can be set as long as seal() has not been called.</li>
* </ul>
*
* Once {@link SealableObject.seal()} has been called on SealableObject, further checks will
* be triggered :
*
* <ul>
* <li>All non final fields cannot be reset without generating a IssueReporter Error.</li>
* <li>All non final fields attempted modification is ignored.</li>
* <li>All fields which are {@link SealableObject} are also sealed.
* </ul>
*
* @param issueReporter issueReporter used by DSL objects.
* @param instantiator function capable of instantiating DSL Types (and other more basic types)
* from a DSL interface.
* @apram propertyChecker function that will be called after a property value was changed to
* perform any checks (like ensuring an exception was raised in the issue reporter.
*/
open class SealableImplementationTester(
private val issueReporter: EvalIssueReporter,
private val instantiator : (KType) -> Any,
private val propertyChecker: (KProperty<*>) -> Unit
) {
val visitedTypes: MutableList<KClass<*>> = mutableListOf()
/**
* Check that a {@link SealableType} subtype has been implemented correctly following checks
* described in the class comment.
*
* @param sealableType DSL type interface (not implementation)
*/
fun checkSealableType(sealableType: KClass<*>) {
visitedTypes+= sealableType
val sealableObject = instantiator(sealableType.createType()) as SealableObject
Truth.assertThat(sealableObject).isInstanceOf(SealableObject::class.java)
checkAllNonFinalFieldsAreProperties(sealableObject)
Truth.assertThat(sealableObject.checkSeal()).isTrue()
val properties = sealableType.memberProperties
// now set all fields, this should always work as the object has not been sealed,
// keep a copy of the object we are setting on each property.
properties.forEach( { property ->
if (property.visibility != KVisibility.PUBLIC) {
System.out.println("Skipping non public ${property.name}")
return@forEach
}
checkPropertyValueIsTested(property.returnType)
if (property is KMutableProperty<*>) {
System.out.println("setting property ${property.name} : ${property.returnType} : ${property.visibility}")
property.isAccessible = true
val propertyValue = instantiator(property.returnType)
property.setter.call(sealableObject, propertyValue)
} else {
System.out.println("reading property ${property.name} : ${property.returnType} : ${property.visibility}")
// this is most likely a property on which there is only a getter, make sure that
// if the returned type is a sealable object it should not be sealed yet.
try {
val propertyValue = property.getter.call(sealableObject)
if (propertyValue is SealableObject) {
Truth.assertWithMessage("Readonly property ${property.name} should not be sealed")
.that(propertyValue.isSealed()).isFalse()
}
} catch(e: InvocationTargetException) {
// only bypass non implemented.
Truth.assertWithMessage("Cannot retrieve ${property.name} value on ${sealableObject::class}")
.that(e.targetException is NotImplementedError)
}
}
})
sealableObject.seal()
Truth.assertThat(sealableObject.isSealed()).isTrue()
properties.forEach( { property ->
System.out.println("testing property ${property.name} : ${property.visibility}")
try {
val propertyValue = property.getter.call(sealableObject)
checkPropertyValueIsSealed(sealableType, property.name, propertyValue)
if (property is KMutableProperty1<*, *> && property.visibility == KVisibility.PUBLIC) {
property.isAccessible = true
property.setter.call(sealableObject, instantiator(property.returnType))
propertyChecker(property)
Mockito.clearInvocations(issueReporter)
// make sure the property was not actually mutated.
Truth.assertWithMessage("Sealed property ${property.name} was mutate")
.that(property.getter.call(sealableObject))
.isEqualTo(propertyValue)
}
} catch(e: InvocationTargetException) {
// only bypass non implemented.
Truth.assertWithMessage("Cannot retrieve ${property.name} value on ${sealableObject::class}")
.that(e.targetException is NotImplementedError)
}
})
}
private fun checkPropertyValueIsSealed(type: KClass<*>, name: String, propertyValue : Any?) {
if (propertyValue is SealableObject) {
Truth.assertWithMessage(
"Property ${type.simpleName}.$name should be sealed")
.that(propertyValue.isSealed())
.isTrue()
return
}
// basic and immutable types are fine
if (propertyValue is Boolean || propertyValue is Double || propertyValue is Int ||
propertyValue is String || propertyValue is Float || propertyValue is File ||
propertyValue is JavaVersion) {
return
}
// anything else is suspicious, throw a test failure.
Assert.fail("Property ${type.simpleName}.$name is not immutable nor sealable." )
}
private fun checkPropertyValueIsTested(propertyType : KType) {
System.out.println("Asserting $propertyType")
if (propertyType.isSubtypeOf(SealableObject::class.createType())) {
visitedTypes+= propertyType.jvmErasure
}
propertyType.arguments.forEach { argument ->
if (argument.type != null) {
checkPropertyValueIsTested(argument.type!!)
}
}
}
private fun checkAllNonFinalFieldsAreProperties(sealableObject: SealableObject) {
val properties = sealableObject::class.allSuperclasses.flatMap {
type -> type.declaredMemberProperties }
val list = collectFields(sealableObject::class.java)
.filter { it -> it.modifiers.and(Modifier.FINAL) == 0
&& it.name != "\$jacocoData"
&& !isDeclaredAsProperty(properties, it) }
Truth.assertThat(list).isEmpty()
}
private fun isDeclaredAsProperty(properties : List<KProperty<*>>, field : Field) : Boolean {
return properties.firstOrNull { property -> property.name == field.name } != null
}
private fun collectFields(type: Class<*>): List<Field> {
return getHierarchy(type).flatMap { it -> it.declaredFields.toList() }
}
private fun getHierarchy(type: Class<*>) : List<Class<*>> {
val typeHierarchy = ArrayList<Class<*>>()
var currentType : Class<*>? = type
while (currentType != null) {
typeHierarchy.add(currentType)
currentType = currentType.superclass
}
return typeHierarchy
}
}