Skip to content

Commit

Permalink
Closes #5: Extensions have to be stateless
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Aug 14, 2020
1 parent 60a8a3c commit 9889d2b
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 81 deletions.
93 changes: 93 additions & 0 deletions src/main/kotlin/com/asarkar/grpc/test/ExtensionContextUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.asarkar.grpc.test

import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.platform.commons.JUnitException
import org.junit.platform.commons.PreconditionViolationException
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import kotlin.reflect.KFunction1

private object ExtensionContextUtils {
internal val NAMESPACE: ExtensionContext.Namespace = ExtensionContext.Namespace
.create(*GrpcCleanupExtension::class.java.name.split(".").toTypedArray())
internal const val RESOURCES = "resources"
internal const val RESOURCES_FIELD = "resources-field"
}

@Suppress("UNCHECKED_CAST")
internal var ExtensionContext.resources: MutableMap<Boolean, MutableList<Resources>>
get() = getStore(ExtensionContextUtils.NAMESPACE)
.getOrDefault(
ExtensionContextUtils.RESOURCES,
MutableMap::class.java,
mutableMapOf<Boolean, MutableList<Resources>>()
) as MutableMap<Boolean, MutableList<Resources>>
set(value) {
getStore(ExtensionContextUtils.NAMESPACE)
.put(ExtensionContextUtils.RESOURCES, value)
}

internal var ExtensionContext.resourcesField: Field?
get() = getStore(ExtensionContextUtils.NAMESPACE)
.get(
ExtensionContextUtils.RESOURCES_FIELD,
Field::class.java
)
set(value) {
getStore(ExtensionContextUtils.NAMESPACE)
.put(ExtensionContextUtils.RESOURCES_FIELD, value)
}

internal var ExtensionContext.resourcesInstance: Resources?
get() {
return try {
val target = testInstance.orElse(null)
resourcesField?.takeIf { target != null || isStaticField }?.get(target) as Resources?
} catch (e: ReflectiveOperationException) {
throw JUnitException("Illegal state: Cannot get Resources field", e)
}
}
set(value) {
try {
val target = testInstance.orElse(null)
resourcesField?.takeIf { target != null || isStaticField }?.set(target, value)
} catch (e: ReflectiveOperationException) {
throw JUnitException("Illegal state: Cannot set Resources field", e)
}
}

private val ExtensionContext.isStaticField: Boolean
get() = resourcesField != null && Modifier.isStatic(resourcesField!!.modifiers)

internal val ExtensionContext.isAccessResourcesField: Boolean
get() = resourcesField != null &&
testInstanceLifecycle.orElse(null) != TestInstance.Lifecycle.PER_CLASS &&
!isStaticField

internal val ExtensionContext.cleanUp: KFunction1<Resources, Unit>
get() = if (executionException.isPresent) Resources::forceCleanUp else Resources::cleanUp

internal fun ExtensionContext.findResourcesField(): Field? {
return generateSequence<Pair<Class<*>?, Field?>>((requiredTestClass to null)) { (clazz, field) ->
val fields = try {
clazz!!.declaredFields.filter { it.type == Resources::class.java }
} catch (e: ReflectiveOperationException) {
throw JUnitException("Illegal state: Cannot find Resources field", e)
}
if (fields.size > 1) {
throw PreconditionViolationException("At most one field of type Resources may be declared by a class")
}
val fld = fields.firstOrNull()
when {
fld != null -> (clazz to fld)
clazz.superclass != null -> (clazz.superclass to field)
else -> (null to field)
}
}
.dropWhile { it.first != null && it.second == null }
.take(1)
.iterator()
.next()
.second
}
101 changes: 20 additions & 81 deletions src/main/kotlin/com/asarkar/grpc/test/GrpcCleanupExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,58 +11,49 @@ import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import org.junit.platform.commons.JUnitException
import org.junit.platform.commons.PreconditionViolationException
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import kotlin.reflect.KFunction1

/**
* A JUnit 5 [Extension](https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/extension/Extension.html) that can register gRPC resources and manages their automatic release at
* A JUnit 5 [Extension](https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/extension/Extension.html)
* that can register gRPC resources and manages their automatic release at
* the end of the test. If any of the registered resources can not be successfully released, fails the test.
* Keeping in line with the [GrpcCleanupRule](https://grpc.github.io/grpc-java/javadoc/io/grpc/testing/GrpcCleanupRule.html),
* tries to force release if the test has already failed.
*
* @author Abhijit Sarkar
* @since 1.0.0
*
* @throws [PreconditionViolationException] if a test method or class declares more than one [Resources].
* @throws [PostconditionViolationException] if one or more registered resources can not be successfully released.
* @throws [JUnitException] if there is an unexpected problem.
*/
class GrpcCleanupExtension :
BeforeEachCallback, AfterEachCallback, ParameterResolver, BeforeAllCallback, AfterAllCallback {
private val resources = mutableMapOf<Boolean, MutableList<Resources>>()
private var resourcesField: Field? = null

override fun beforeEach(ctx: ExtensionContext) {
val resources = ctx.requiredTestMethod.parameters
.filter { it.type == Resources::class.java }
if (resources.size > 1) {
throw PreconditionViolationException("At most one parameter of type Resources may be declared by a method")
}

if (shouldAccessResourcesField(ctx)) {
if (tryGet(ctx.requiredTestInstance) != null) {
if (ctx.isAccessResourcesField) {
if (ctx.resourcesInstance != null) {
throw PreconditionViolationException(
"Either set lifecycle PER_CLASS or don't initialize Resources field"
)
}
trySet(ctx.requiredTestInstance)
ctx.resourcesInstance = Resources()
}
}

override fun afterEach(ctx: ExtensionContext) {
var successful = true
this.resources[false]?.forEach {
getCleanUpMethod(ctx)(it)
ctx.resources[false]?.forEach {
ctx.cleanUp(it)
successful = it.awaitReleased()
}

if (shouldAccessResourcesField(ctx)) {
tryGet(ctx.requiredTestInstance)?.also {
getCleanUpMethod(ctx)(it)
if (ctx.isAccessResourcesField) {
ctx.resourcesInstance?.also {
ctx.cleanUp(it)
successful = it.awaitReleased()

trySet(ctx.requiredTestInstance, null)
ctx.resourcesInstance = null
}
}
if (!successful) throw PostconditionViolationException("One or more Resources couldn't be released")
Expand All @@ -79,87 +70,35 @@ class GrpcCleanupExtension :
parameterCtx.declaringExecutable.isAnnotationPresent(BeforeAll::class.java)
)

return Resources().also { resources.getOrPut(once, { mutableListOf() }).add(it) }
return Resources().also { extensionCtx.resources.getOrPut(once, { mutableListOf() }).add(it) }
}

override fun beforeAll(ctx: ExtensionContext) {
val field = findField(ctx)
val field = ctx.findResourcesField()

if (field != null) {
try {
field.isAccessible = true
} catch (e: ReflectiveOperationException) {
throw JUnitException("Illegal state: Cannot access Resources field", e)
}
resourcesField = field
if (tryGet(ctx.testInstance.orElse(null)) == null) {
trySet(ctx.testInstance.orElse(null))
ctx.resourcesField = field
if (ctx.resourcesInstance == null) {
ctx.resourcesInstance = Resources()
}
}
}

override fun afterAll(ctx: ExtensionContext) {
var successful = true
tryGet(ctx.testInstance.orElse(null))?.also {
getCleanUpMethod(ctx)(it)
ctx.resourcesInstance?.also {
ctx.cleanUp(it)
successful = it.awaitReleased()
}
this.resources[true]?.forEach {
getCleanUpMethod(ctx)(it)
ctx.resources[true]?.forEach {
ctx.cleanUp(it)
successful = it.awaitReleased()
}
if (!successful) throw PostconditionViolationException("One or more Resources couldn't be released")
}

private fun shouldAccessResourcesField(ctx: ExtensionContext): Boolean {
return resourcesField != null &&
ctx.testInstanceLifecycle.orElse(null) != TestInstance.Lifecycle.PER_CLASS &&
!resourcesField.isStatic()
}

private fun trySet(target: Any?, value: Resources? = Resources()) {
try {
resourcesField?.takeIf { target != null || it.isStatic() }?.set(target, value)
} catch (e: ReflectiveOperationException) {
throw JUnitException("Illegal state: Cannot set Resources field", e)
}
}

private fun tryGet(target: Any?): Resources? {
return try {
resourcesField?.takeIf { target != null || it.isStatic() }?.get(target) as Resources?
} catch (e: ReflectiveOperationException) {
throw JUnitException("Illegal state: Cannot get Resources field", e)
}
}

private fun getCleanUpMethod(ctx: ExtensionContext): KFunction1<Resources, Unit> {
return if (ctx.executionException.isPresent) Resources::forceCleanUp else Resources::cleanUp
}

private fun Field?.isStatic() = this != null && Modifier.isStatic(this.modifiers)

private fun findField(ctx: ExtensionContext): Field? {
return generateSequence<Pair<Class<*>?, Field?>>((ctx.requiredTestClass to null)) { (clazz, field) ->
val fields = try {
clazz!!.declaredFields.filter { it.type == Resources::class.java }
} catch (e: ReflectiveOperationException) {
throw JUnitException("Illegal state: Cannot find Resources field", e)
}
if (fields.size > 1) {
throw PreconditionViolationException("At most one field of type Resources may be declared by a class")
}
val fld = fields.firstOrNull()
when {
fld != null -> (clazz to fld)
clazz.superclass != null -> (clazz.superclass to field)
else -> (null to field)
}
}
.dropWhile { it.first != null && it.second == null }
.take(1)
.iterator()
.next()
.second
}
}

0 comments on commit 9889d2b

Please sign in to comment.