diff --git a/src/main/kotlin/com/asarkar/grpc/test/ExtensionContextUtils.kt b/src/main/kotlin/com/asarkar/grpc/test/ExtensionContextUtils.kt new file mode 100644 index 0000000..85d1733 --- /dev/null +++ b/src/main/kotlin/com/asarkar/grpc/test/ExtensionContextUtils.kt @@ -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> + get() = getStore(ExtensionContextUtils.NAMESPACE) + .getOrDefault( + ExtensionContextUtils.RESOURCES, + MutableMap::class.java, + mutableMapOf>() + ) as MutableMap> + 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 + get() = if (executionException.isPresent) Resources::forceCleanUp else Resources::cleanUp + +internal fun ExtensionContext.findResourcesField(): Field? { + return generateSequence?, 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 +} diff --git a/src/main/kotlin/com/asarkar/grpc/test/GrpcCleanupExtension.kt b/src/main/kotlin/com/asarkar/grpc/test/GrpcCleanupExtension.kt index 179bf8e..8307bf3 100644 --- a/src/main/kotlin/com/asarkar/grpc/test/GrpcCleanupExtension.kt +++ b/src/main/kotlin/com/asarkar/grpc/test/GrpcCleanupExtension.kt @@ -11,28 +11,19 @@ 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>() - private var resourcesField: Field? = null - override fun beforeEach(ctx: ExtensionContext) { val resources = ctx.requiredTestMethod.parameters .filter { it.type == Resources::class.java } @@ -40,29 +31,29 @@ class GrpcCleanupExtension : 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") @@ -79,11 +70,11 @@ 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 { @@ -91,75 +82,23 @@ class GrpcCleanupExtension : } 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 { - 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?, 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 - } }