Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #5: Extensions have to be stateless #6

Merged
merged 1 commit into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}