Skip to content

Commit

Permalink
Test Engine can select/filter tests by classname pattern and unique ID
Browse files Browse the repository at this point in the history
  • Loading branch information
npryce committed Nov 24, 2018
1 parent 4305503 commit 5c9678c
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,57 @@ import com.oneeyedmen.minutest.RuntimeContext
import com.oneeyedmen.minutest.RuntimeNode
import com.oneeyedmen.minutest.RuntimeTest
import com.oneeyedmen.minutest.experimental.TopLevelContextBuilder
import org.junit.platform.engine.*
import org.junit.platform.engine.EngineDiscoveryRequest
import org.junit.platform.engine.EngineExecutionListener
import org.junit.platform.engine.ExecutionRequest
import org.junit.platform.engine.TestDescriptor
import org.junit.platform.engine.TestDescriptor.Type
import org.junit.platform.engine.TestDescriptor.Type.CONTAINER
import org.junit.platform.engine.TestDescriptor.Type.TEST
import org.junit.platform.engine.TestEngine
import org.junit.platform.engine.TestExecutionResult
import org.junit.platform.engine.TestSource
import org.junit.platform.engine.TestTag
import org.junit.platform.engine.UniqueId
import org.junit.platform.engine.discovery.UniqueIdSelector
import org.junit.platform.engine.support.descriptor.ClassSource
import org.junit.platform.engine.support.descriptor.EngineDescriptor
import org.opentest4j.AssertionFailedError
import java.util.Optional
import kotlin.collections.LinkedHashSet
import kotlin.reflect.KProperty0
import kotlin.reflect.jvm.javaMethod
import kotlin.reflect.jvm.jvmName


class MinutestTestEngine : TestEngine {
override fun getId() = Companion.id
override fun getId() = engineId

override fun discover(discoveryRequest: EngineDiscoveryRequest, uniqueId: UniqueId): TestDescriptor {
val root = MinutestEngineDescriptor(uniqueId)
scan(discoveryRequest).forEach(root::addChild)
val root = MinutestEngineDescriptor(uniqueId, discoveryRequest)
scan(root, discoveryRequest).forEach(root::addChild)
return root
}

override fun execute(request: ExecutionRequest) {
execute(request.rootTestDescriptor, request.engineExecutionListener)
val root = request.rootTestDescriptor
if (root is MinutestEngineDescriptor) {
execute(root, root.discoveryRequest, request.engineExecutionListener)
}
else {
throw IllegalArgumentException("root descriptor is not a ${MinutestEngineDescriptor::class.jvmName}")
}
}

private fun execute(test: TestDescriptor, listener: EngineExecutionListener) {
listener.executionStarted(test)
private fun execute(descriptor: TestDescriptor, request: EngineDiscoveryRequest, listener: EngineExecutionListener) {
listener.executionStarted(descriptor)
val result = try {
when (test) {
when (descriptor) {
is TopLevelContextDescriptor ->
executeTopLevelContextDescriptor(test, listener)
executeTopLevelContextDescriptor(descriptor, request, listener)
is MinutestNodeDescriptor ->
executeNodeDescriptor(test, listener)
executeNodeDescriptor(descriptor, request, listener)
else ->
executeChildren(test, listener)
executeDiscoveredChildTests(descriptor, request, listener)
}
TestExecutionResult.successful()
}
Expand All @@ -50,74 +65,70 @@ class MinutestTestEngine : TestEngine {
TestExecutionResult.aborted(t)
}

listener.executionFinished(test, result)
listener.executionFinished(descriptor, result)
}

private fun executeTopLevelContextDescriptor(descriptor: TopLevelContextDescriptor, listener: EngineExecutionListener) {
executeMinutestNode(descriptor, descriptor.instantiate(), listener)
private fun executeTopLevelContextDescriptor(descriptor: TopLevelContextDescriptor, request: EngineDiscoveryRequest, listener: EngineExecutionListener) {
executeMinutestNode(descriptor, descriptor.instantiate(), request, listener)
}

private fun executeNodeDescriptor(descriptor: MinutestNodeDescriptor, listener: EngineExecutionListener) {
executeMinutestNode(descriptor, descriptor.node, listener)
private fun executeNodeDescriptor(descriptor: MinutestNodeDescriptor, request: EngineDiscoveryRequest, listener: EngineExecutionListener) {
executeMinutestNode(descriptor, descriptor.node, request, listener)
}

private fun executeMinutestNode(descriptor: TestDescriptor, node: RuntimeNode, listener: EngineExecutionListener) {
private fun executeMinutestNode(descriptor: TestDescriptor, node: RuntimeNode, request: EngineDiscoveryRequest, listener: EngineExecutionListener) =
when (node) {
is RuntimeContext -> {
childDescriptors(node).forEach { child ->
descriptor.addChild(child)
listener.dynamicTestRegistered(child)

execute(child, listener)
childrenAsDescriptors(descriptor, node).forEach { child ->
if (request.selectsByUniqueId(child)) {
descriptor.addChild(child)
listener.dynamicTestRegistered(child)
execute(child, request, listener)
}
}
}
is RuntimeTest -> {
is RuntimeTest ->
node.run()
}
}
}

private fun childDescriptors(context: RuntimeContext) =
context.children.map { MinutestNodeDescriptor(it) }
private fun childrenAsDescriptors(parentDescriptor: TestDescriptor, parentNode: RuntimeContext) =
parentNode.children.map { node -> MinutestNodeDescriptor(parentDescriptor, node) }

private fun executeChildren(test: TestDescriptor, listener: EngineExecutionListener) {
test.children.forEach {
execute(it, listener)
private fun executeDiscoveredChildTests(test: TestDescriptor, request: EngineDiscoveryRequest, listener: EngineExecutionListener) {
test.children.forEach { child ->
if (request.selectsByUniqueId(child)) {
execute(child, request, listener)
}
}
}

companion object {
val id = "minutest"
val engineId = "minutest"
}
}

class MinutestEngineDescriptor(uniqueId: UniqueId) :
class MinutestEngineDescriptor(uniqueId: UniqueId, val discoveryRequest: EngineDiscoveryRequest) :
EngineDescriptor(uniqueId, "Minutest")


sealed class MinutestDescriptor(
protected var _parent: TestDescriptor? = null
) : TestDescriptor {
private val packageType = "minutest-package"
private val contextType = "minutest-context"
private val testType = "minutest-test"


internal sealed class MinutestDescriptor(parent: TestDescriptor, idType: String, id: String) : TestDescriptor {
private var _parent: TestDescriptor? = parent
private val _children = LinkedHashSet<TestDescriptor>()
private val _uniqueId = parent.uniqueId.append(idType, id)

final override fun getParent() =
Optional.ofNullable(_parent)

final override fun getUniqueId() = _uniqueId
final override fun getParent() = Optional.ofNullable(_parent)
final override fun setParent(parent: TestDescriptor?) {
_parent = parent
}

final override fun getUniqueId() =
_parent?.uniqueId?.append(idType, id) ?: UniqueId.root(idType, id)

abstract val idType: String
abstract val id: String

override fun mayRegisterTests(): Boolean = true

final override fun getChildren(): Set<TestDescriptor> {
return _children.toSet()
}
final override fun mayRegisterTests(): Boolean = true
final override fun getChildren() = _children.toMutableSet()

override fun addChild(descriptor: TestDescriptor) {
_children.add(descriptor)
Expand All @@ -134,63 +145,72 @@ sealed class MinutestDescriptor(
_parent = null
}

override fun findByUniqueId(uniqueId: UniqueId?) =
Optional.empty<TestDescriptor>()

override fun getTags() =
emptySet<TestTag>()
override fun findByUniqueId(uniqueId: UniqueId?): Optional<TestDescriptor> =
if (uniqueId == this._uniqueId) {
Optional.of(this)
}
else {
_children.asSequence().map { findByUniqueId(uniqueId) }.firstOrNull { it.isPresent } ?: Optional.empty()
}

override fun getTags() = emptySet<TestTag>()
}

class TestPackageDescriptor(
internal class TestPackageDescriptor(
parent: TestDescriptor,
private val packageName: String,
children: List<TopLevelContextDescriptor>
): MinutestDescriptor() {
children: List<TopLevelContextDescriptor> = emptyList()
) : MinutestDescriptor(parent, packageType, packageName) {
init {
children.forEach { addChild(it) }
}

override fun getType(): Type = CONTAINER
override fun getDisplayName() = packageName
override val idType: String = "minutest-package"
override val id: String = packageName
override fun getSource(): Optional<TestSource> =
// Optional.of(PackageSource.from(packageName))
Optional.empty() // True implementation commented out because Gradle doesn't handle it correctly

}

class TopLevelContextDescriptor(

internal class TopLevelContextDescriptor(
parent: TestDescriptor,
val property: KProperty0<TopLevelContextBuilder>
) : MinutestDescriptor() {
) : MinutestDescriptor(parent, contextType, property.name) {

override fun getType(): Type = CONTAINER
override fun getDisplayName() = property.name
override val idType: String = "minutest-property"
override val id: String = property.name
override fun getSource(): Optional<TestSource> =
// Optional.ofNullable(MethodSource.from(property.getter.javaMethod))
Optional.ofNullable(ClassSource.from(property.getter.javaMethod?.declaringClass)) // True implementation commented out because Gradle doesn't handle it correctly

fun instantiate() = property.get().build(property.name)
}

class MinutestNodeDescriptor(

internal class MinutestNodeDescriptor(
parent: TestDescriptor,
val node: RuntimeNode
) : MinutestDescriptor() {

) : MinutestDescriptor(parent, node.descriptorIdType(), node.name) {
override fun getType() = when (node) {
is RuntimeContext -> CONTAINER
is RuntimeTest -> TEST
}

override val idType: String = when (node) {
is RuntimeContext -> "minutest-context"
is RuntimeTest -> "minutest-test"
}

override val id: String = node.name
override fun getDisplayName() = node.name
override fun getSource() = Optional.empty<TestSource>()
}

private fun RuntimeNode.descriptorIdType(): String {
return when (this) {
is RuntimeContext -> contextType
is RuntimeTest -> testType
}
}

private fun EngineDiscoveryRequest.selectsByUniqueId(descriptor: TestDescriptor) =
getSelectorsByType<UniqueIdSelector>()
.run { isEmpty() || any { selector -> descriptor.uniqueId.overlaps(selector.uniqueId) } }

private fun UniqueId.overlaps(that: UniqueId) =
this.hasPrefix(that) || that.hasPrefix(this)
20 changes: 16 additions & 4 deletions core/src/main/kotlin/com/oneeyedmen/minutest/junit/scanning.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import io.github.classgraph.ClassGraph
import io.github.classgraph.ClassRefTypeSignature
import io.github.classgraph.FieldInfo
import io.github.classgraph.TypeSignature
import org.junit.platform.engine.DiscoveryFilter
import org.junit.platform.engine.DiscoverySelector
import org.junit.platform.engine.EngineDiscoveryRequest
import org.junit.platform.engine.Filter
import org.junit.platform.engine.discovery.ClassNameFilter
import org.junit.platform.engine.discovery.ClassSelector
import org.junit.platform.engine.discovery.DirectorySelector
import org.junit.platform.engine.discovery.PackageSelector
Expand All @@ -19,11 +22,14 @@ import kotlin.reflect.jvm.kotlinProperty
inline fun <reified T : DiscoverySelector> EngineDiscoveryRequest.getSelectorsByType(): List<T> =
getSelectorsByType(T::class.java)

inline fun <U, reified T : DiscoveryFilter<U>> EngineDiscoveryRequest.getFiltersByType(): Filter<U> =
Filter.composeFilters(getFiltersByType(T::class.java))

inline fun <reified T : DiscoverySelector> EngineDiscoveryRequest.forEach(block: (T) -> Unit) {
getSelectorsByType<T>().forEach(block)
}

fun scan(rq: EngineDiscoveryRequest): List<TestPackageDescriptor> {
internal fun scan(root: MinutestEngineDescriptor, rq: EngineDiscoveryRequest): List<TestPackageDescriptor> {
val scanner = ClassGraph()
.enableClassInfo()
.enableFieldInfo()
Expand All @@ -39,13 +45,19 @@ fun scan(rq: EngineDiscoveryRequest): List<TestPackageDescriptor> {

return scanned
.allClasses
.filter { rq.getFiltersByType<String,ClassNameFilter>().apply(it.name).included() }
.flatMap { it.declaredFieldInfo }
.filter { it.isTopLevelContext() }
.mapNotNull { it.toKotlinProperty() }
.filter { it.visibility == PUBLIC }
.map { property -> TopLevelContextDescriptor(property) }
.groupBy { it.property.javaField?.declaringClass?.`package`?.name ?: "<tests>" }
.map { (packageName, tests) -> TestPackageDescriptor(packageName, tests) }
.groupBy { it.javaField?.declaringClass?.`package`?.name ?: "<tests>" }
.map { (packageName, properties) ->
TestPackageDescriptor(root, packageName).also { packageDescriptor ->
properties.forEach { property ->
packageDescriptor.addChild(TopLevelContextDescriptor(packageDescriptor, property))
}
}
}
}

private fun FieldInfo.toKotlinProperty(): KProperty0<TopLevelContextBuilder>? {
Expand Down
Loading

0 comments on commit 5c9678c

Please sign in to comment.